aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Database
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
committerGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
commit98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch)
tree131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Firebase/Database
parent32461366c9e204a527ca05e6e9b9404a2454ac51 (diff)
Initial
Diffstat (limited to 'Firebase/Database')
-rw-r--r--Firebase/Database/Api/FIRDataEventType.h39
-rw-r--r--Firebase/Database/Api/FIRDataSnapshot.h148
-rw-r--r--Firebase/Database/Api/FIRDataSnapshot.m101
-rw-r--r--Firebase/Database/Api/FIRDatabase.h140
-rw-r--r--Firebase/Database/Api/FIRDatabase.m268
-rw-r--r--Firebase/Database/Api/FIRDatabaseConfig.h63
-rw-r--r--Firebase/Database/Api/FIRDatabaseConfig.m117
-rw-r--r--Firebase/Database/Api/FIRDatabaseQuery.h315
-rw-r--r--Firebase/Database/Api/FIRDatabaseQuery.m525
-rw-r--r--Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h29
-rw-r--r--Firebase/Database/Api/FIRMutableData.h130
-rw-r--r--Firebase/Database/Api/FIRMutableData.m134
-rw-r--r--Firebase/Database/Api/FIRServerValue.h35
-rw-r--r--Firebase/Database/Api/FIRServerValue.m30
-rw-r--r--Firebase/Database/Api/FIRTransactionResult.h47
-rw-r--r--Firebase/Database/Api/FIRTransactionResult.m39
-rw-r--r--Firebase/Database/Api/FirebaseDatabase.h29
-rw-r--r--Firebase/Database/Api/Private/FIRDataSnapshot_Private.h27
-rw-r--r--Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h43
-rw-r--r--Firebase/Database/Api/Private/FIRDatabaseReference_Private.h29
-rw-r--r--Firebase/Database/Api/Private/FIRDatabase_Private.h28
-rw-r--r--Firebase/Database/Api/Private/FIRMutableData_Private.h26
-rw-r--r--Firebase/Database/Api/Private/FIRTransactionResult_Private.h25
-rw-r--r--Firebase/Database/Api/Private/FTypedefs_Private.h56
-rw-r--r--Firebase/Database/Constants/FConstants.h190
-rw-r--r--Firebase/Database/Constants/FConstants.m183
-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
-rw-r--r--Firebase/Database/FClock.h35
-rw-r--r--Firebase/Database/FClock.m58
-rw-r--r--Firebase/Database/FEventGenerator.h27
-rw-r--r--Firebase/Database/FEventGenerator.m141
-rw-r--r--Firebase/Database/FIRDatabaseConfig_Private.h35
-rw-r--r--Firebase/Database/FIRDatabaseReference.h719
-rw-r--r--Firebase/Database/FIRDatabaseReference.m404
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj438
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h37
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m282
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch7
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h71
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m158
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h38
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m131
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h43
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m87
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h45
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h45
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m245
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h25
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m99
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist22
-rw-r--r--Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings2
-rw-r--r--Firebase/Database/FIndex.h50
-rw-r--r--Firebase/Database/FIndex.m38
-rw-r--r--Firebase/Database/FKeyIndex.h23
-rw-r--r--Firebase/Database/FKeyIndex.m115
-rw-r--r--Firebase/Database/FListenComplete.h29
-rw-r--r--Firebase/Database/FListenComplete.m51
-rw-r--r--Firebase/Database/FMaxNode.h23
-rw-r--r--Firebase/Database/FMaxNode.m61
-rw-r--r--Firebase/Database/FNamedNode.h32
-rw-r--r--Firebase/Database/FNamedNode.m94
-rw-r--r--Firebase/Database/FPathIndex.h23
-rw-r--r--Firebase/Database/FPathIndex.m125
-rw-r--r--Firebase/Database/FPriorityIndex.h23
-rw-r--r--Firebase/Database/FPriorityIndex.m118
-rw-r--r--Firebase/Database/FRangedFilter.h32
-rw-r--r--Firebase/Database/FRangedFilter.m118
-rw-r--r--Firebase/Database/FTransformedEnumerator.h24
-rw-r--r--Firebase/Database/FTransformedEnumerator.m43
-rw-r--r--Firebase/Database/FTreeSortedDictionary.h46
-rw-r--r--Firebase/Database/FTreeSortedDictionary.m342
-rw-r--r--Firebase/Database/FValueIndex.h23
-rw-r--r--Firebase/Database/FValueIndex.m106
-rw-r--r--Firebase/Database/FViewProcessor.h41
-rw-r--r--Firebase/Database/FViewProcessor.m654
-rw-r--r--Firebase/Database/FViewProcessorResult.h30
-rw-r--r--Firebase/Database/FViewProcessorResult.m35
-rw-r--r--Firebase/Database/Firebase-Prefix.pch7
-rw-r--r--Firebase/Database/FirebaseDatabase.podspec48
-rw-r--r--Firebase/Database/Info.plist26
-rw-r--r--Firebase/Database/Login/FAuthTokenProvider.h36
-rw-r--r--Firebase/Database/Login/FAuthTokenProvider.m162
-rw-r--r--Firebase/Database/Login/FIRNoopAuthTokenProvider.h22
-rw-r--r--Firebase/Database/Login/FIRNoopAuthTokenProvider.m33
-rw-r--r--Firebase/Database/Persistence/FCachePolicy.h41
-rw-r--r--Firebase/Database/Persistence/FCachePolicy.m79
-rw-r--r--Firebase/Database/Persistence/FLevelDBStorageEngine.h37
-rw-r--r--Firebase/Database/Persistence/FLevelDBStorageEngine.m717
-rw-r--r--Firebase/Database/Persistence/FPendingPut.h55
-rw-r--r--Firebase/Database/Persistence/FPendingPut.m112
-rw-r--r--Firebase/Database/Persistence/FPersistenceManager.h52
-rw-r--r--Firebase/Database/Persistence/FPersistenceManager.m190
-rw-r--r--Firebase/Database/Persistence/FPruneForest.h38
-rw-r--r--Firebase/Database/Persistence/FPruneForest.m177
-rw-r--r--Firebase/Database/Persistence/FStorageEngine.h53
-rw-r--r--Firebase/Database/Persistence/FTrackedQuery.h40
-rw-r--r--Firebase/Database/Persistence/FTrackedQuery.m102
-rw-r--r--Firebase/Database/Persistence/FTrackedQueryManager.h51
-rw-r--r--Firebase/Database/Persistence/FTrackedQueryManager.m321
-rw-r--r--Firebase/Database/Realtime/FConnection.h52
-rw-r--r--Firebase/Database/Realtime/FConnection.m211
-rw-r--r--Firebase/Database/Realtime/FWebSocketConnection.h46
-rw-r--r--Firebase/Database/Realtime/FWebSocketConnection.m305
-rw-r--r--Firebase/Database/Snapshot/FChildrenNode.h40
-rw-r--r--Firebase/Database/Snapshot/FChildrenNode.m385
-rw-r--r--Firebase/Database/Snapshot/FCompoundWrite.h61
-rw-r--r--Firebase/Database/Snapshot/FCompoundWrite.m257
-rw-r--r--Firebase/Database/Snapshot/FEmptyNode.h24
-rw-r--r--Firebase/Database/Snapshot/FEmptyNode.m29
-rw-r--r--Firebase/Database/Snapshot/FIndexedNode.h49
-rw-r--r--Firebase/Database/Snapshot/FIndexedNode.m202
-rw-r--r--Firebase/Database/Snapshot/FLeafNode.h28
-rw-r--r--Firebase/Database/Snapshot/FLeafNode.m250
-rw-r--r--Firebase/Database/Snapshot/FNode.h46
-rw-r--r--Firebase/Database/Snapshot/FSnapshotUtilities.h45
-rw-r--r--Firebase/Database/Snapshot/FSnapshotUtilities.m301
-rw-r--r--Firebase/Database/Utilities/FAtomicNumber.h23
-rw-r--r--Firebase/Database/Utilities/FAtomicNumber.m54
-rw-r--r--Firebase/Database/Utilities/FEventEmitter.h33
-rw-r--r--Firebase/Database/Utilities/FEventEmitter.m145
-rw-r--r--Firebase/Database/Utilities/FNextPushId.h23
-rw-r--r--Firebase/Database/Utilities/FNextPushId.m63
-rw-r--r--Firebase/Database/Utilities/FParsedUrl.h25
-rw-r--r--Firebase/Database/Utilities/FParsedUrl.m24
-rw-r--r--Firebase/Database/Utilities/FStringUtilities.h26
-rw-r--r--Firebase/Database/Utilities/FStringUtilities.m61
-rw-r--r--Firebase/Database/Utilities/FTypedefs.h45
-rw-r--r--Firebase/Database/Utilities/FUtilities.h76
-rw-r--r--Firebase/Database/Utilities/FUtilities.m389
-rw-r--r--Firebase/Database/Utilities/FValidation.h45
-rw-r--r--Firebase/Database/Utilities/FValidation.m312
-rw-r--r--Firebase/Database/Utilities/NSString+FURLUtils.h24
-rw-r--r--Firebase/Database/Utilities/NSString+FURLUtils.m38
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m22
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleFirebase.h26
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleFirebase.m25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleNodePath.h28
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleNodePath.m33
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjectNode.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjectNode.m32
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjects.h24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleObjects.m24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m26
-rw-r--r--Firebase/Database/Utilities/Tuples/FTuplePathValue.h25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTuplePathValue.m38
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h30
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m37
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m33
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleStringNode.h27
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleStringNode.m34
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTSN.h25
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTSN.m24
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTransaction.h74
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleTransaction.m38
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleUserCallback.h31
-rw-r--r--Firebase/Database/Utilities/Tuples/FTupleUserCallback.m35
-rw-r--r--Firebase/Database/module.modulemap13
-rw-r--r--Firebase/Database/third_party/SocketRocket/FSRWebSocket.h107
-rw-r--r--Firebase/Database/third_party/SocketRocket/FSRWebSocket.m1848
-rw-r--r--Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h23
-rw-r--r--Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m37
-rw-r--r--Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb33
-rw-r--r--Firebase/Database/third_party/SocketRocket/fbase64.c318
-rw-r--r--Firebase/Database/third_party/SocketRocket/fbase64.h33
-rw-r--r--Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h105
-rw-r--r--Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm500
254 files changed, 27936 insertions, 0 deletions
diff --git a/Firebase/Database/Api/FIRDataEventType.h b/Firebase/Database/Api/FIRDataEventType.h
new file mode 100644
index 0000000..fccc98a
--- /dev/null
+++ b/Firebase/Database/Api/FIRDataEventType.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.
+ */
+
+#ifndef Firebase_FIRDataEventType_h
+#define Firebase_FIRDataEventType_h
+
+#import <Foundation/Foundation.h>
+#import "FIRDatabaseSwiftNameSupport.h"
+
+/**
+ * This enum is the set of events that you can observe at a Firebase Database location.
+ */
+typedef NS_ENUM(NSInteger, FIRDataEventType) {
+ /// A new child node is added to a location.
+ FIRDataEventTypeChildAdded,
+ /// A child node is removed from a location.
+ FIRDataEventTypeChildRemoved,
+ /// A child node at a location changes.
+ FIRDataEventTypeChildChanged,
+ /// A child node moves relative to the other child nodes at a location.
+ FIRDataEventTypeChildMoved,
+ /// Any data changes at a location or, recursively, at any child node.
+ FIRDataEventTypeValue
+} FIR_SWIFT_NAME(DataEventType);
+
+#endif
diff --git a/Firebase/Database/Api/FIRDataSnapshot.h b/Firebase/Database/Api/FIRDataSnapshot.h
new file mode 100644
index 0000000..e615260
--- /dev/null
+++ b/Firebase/Database/Api/FIRDataSnapshot.h
@@ -0,0 +1,148 @@
+/*
+ * 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 "FIRDatabaseSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDatabaseReference;
+
+/**
+ * A FIRDataSnapshot contains data from a Firebase Database location. Any time you read
+ * Firebase data, you receive the data as a FIRDataSnapshot.
+ *
+ * FIRDataSnapshots are passed to the blocks you attach with observeEventType:withBlock: or observeSingleEvent:withBlock:.
+ * They are efficiently-generated immutable copies of the data at a Firebase Database location.
+ * They can't be modified and will never change. To modify data at a location,
+ * use a FIRDatabaseReference (e.g. with setValue:).
+ */
+FIR_SWIFT_NAME(DataSnapshot)
+@interface FIRDataSnapshot : NSObject
+
+
+#pragma mark - Navigating and inspecting a snapshot
+
+/**
+ * Gets a FIRDataSnapshot for the location at the specified relative path.
+ * The relative path can either be a simple child key (e.g. 'fred')
+ * or a deeper slash-separated path (e.g. 'fred/name/first'). If the child
+ * location has no data, an empty FIRDataSnapshot is returned.
+ *
+ * @param childPathString A relative path to the location of child data.
+ * @return The FIRDataSnapshot for the child location.
+ */
+- (FIRDataSnapshot *)childSnapshotForPath:(NSString *)childPathString;
+
+
+/**
+ * Return YES if the specified child exists.
+ *
+ * @param childPathString A relative path to the location of a potential child.
+ * @return YES if data exists at the specified childPathString, else NO.
+ */
+- (BOOL) hasChild:(NSString *)childPathString;
+
+
+/**
+ * Return YES if the DataSnapshot has any children.
+ *
+ * @return YES if this snapshot has any children, else NO.
+ */
+- (BOOL) hasChildren;
+
+
+/**
+ * Return YES if the DataSnapshot contains a non-null value.
+ *
+ * @return YES if this snapshot contains a non-null value, else NO.
+ */
+- (BOOL) exists;
+
+
+#pragma mark - Data export
+
+/**
+ * Returns the raw value at this location, coupled with any metadata, such as priority.
+ *
+ * Priorities, where they exist, are accessible under the ".priority" key in instances of NSDictionary.
+ * For leaf locations with priorities, the value will be under the ".value" key.
+ */
+- (id __nullable) valueInExportFormat;
+
+
+#pragma mark - Properties
+
+/**
+ * Returns the contents of this data snapshot as native types.
+ *
+ * Data types returned:
+ * + NSDictionary
+ * + NSArray
+ * + NSNumber (also includes booleans)
+ * + NSString
+ *
+ * @return The data as a native object.
+ */
+@property (strong, readonly, nonatomic, nullable) id value;
+
+
+/**
+ * Gets the number of children for this DataSnapshot.
+ *
+ * @return An integer indicating the number of children.
+ */
+@property (readonly, nonatomic) NSUInteger childrenCount;
+
+
+/**
+ * Gets a FIRDatabaseReference for the location that this data came from.
+ *
+ * @return A FIRDatabaseReference instance for the location of this data.
+ */
+@property (nonatomic, readonly, strong) FIRDatabaseReference * ref;
+
+
+/**
+ * The key of the location that generated this FIRDataSnapshot.
+ *
+ * @return An NSString containing the key for the location of this FIRDataSnapshot.
+ */
+@property (strong, readonly, nonatomic) NSString* key;
+
+
+/**
+ * An iterator for snapshots of the child nodes in this snapshot.
+ * You can use the native for..in syntax:
+ *
+ * for (FIRDataSnapshot* child in snapshot.children) {
+ * ...
+ * }
+ *
+ * @return An NSEnumerator of the children.
+ */
+@property (strong, readonly, nonatomic) NSEnumerator* children;
+
+/**
+ * The priority of the data in this FIRDataSnapshot.
+ *
+ * @return The priority as a string, or nil if no priority was set.
+ */
+@property (strong, readonly, nonatomic, nullable) id priority;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDataSnapshot.m b/Firebase/Database/Api/FIRDataSnapshot.m
new file mode 100644
index 0000000..9559c38
--- /dev/null
+++ b/Firebase/Database/Api/FIRDataSnapshot.m
@@ -0,0 +1,101 @@
+/*
+ * 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 "FIRDataSnapshot.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FChildrenNode.h"
+#import "FValidation.h"
+#import "FTransformedEnumerator.h"
+#import "FIRDatabaseReference.h"
+
+@interface FIRDataSnapshot ()
+@property (nonatomic, strong) FIRDatabaseReference *ref;
+@end
+
+@implementation FIRDataSnapshot
+
+- (id)initWithRef:(FIRDatabaseReference *)ref indexedNode:(FIndexedNode *)node
+{
+ self = [super init];
+ if (self != nil) {
+ self->_ref = ref;
+ self->_node = node;
+ }
+ return self;
+}
+
+- (id) value {
+ return [self.node.node val];
+}
+
+- (id) valueInExportFormat {
+ return [self.node.node valForExport:YES];
+}
+
+- (FIRDataSnapshot *)childSnapshotForPath:(NSString *)childPathString {
+ [FValidation validateFrom:@"child:" validPathString:childPathString];
+ FPath* childPath = [[FPath alloc] initWith:childPathString];
+ FIRDatabaseReference * childRef = [self.ref child:childPathString];
+
+ id<FNode> childNode = [self.node.node getChild:childPath];
+ return [[FIRDataSnapshot alloc] initWithRef:childRef indexedNode:[FIndexedNode indexedNodeWithNode:childNode]];
+}
+
+- (BOOL) hasChild:(NSString *)childPathString {
+ [FValidation validateFrom:@"hasChild:" validPathString:childPathString];
+ FPath* childPath = [[FPath alloc] initWith:childPathString];
+ return ! [[self.node.node getChild:childPath] isEmpty];
+}
+
+- (id) priority {
+ id<FNode> priority = [self.node.node getPriority];
+ return priority.val;
+}
+
+
+- (BOOL) hasChildren {
+ if([self.node.node isLeafNode]) {
+ return false;
+ }
+ else {
+ return ![self.node.node isEmpty];
+ }
+}
+
+- (BOOL) exists {
+ return ![self.node.node isEmpty];
+}
+
+- (NSString *) key {
+ return [self.ref key];
+}
+
+- (NSUInteger) childrenCount {
+ return [self.node.node numChildren];
+}
+
+- (NSEnumerator *) children {
+ return [[FTransformedEnumerator alloc] initWithEnumerator:self.node.childEnumerator andTransform:^id(FNamedNode *node) {
+ FIRDatabaseReference *childRef = [self.ref child:node.name];
+ return [[FIRDataSnapshot alloc] initWithRef:childRef indexedNode:[FIndexedNode indexedNodeWithNode:node.node]];
+ }];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"Snap (%@) %@", self.key, self.node.node];
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabase.h b/Firebase/Database/Api/FIRDatabase.h
new file mode 100644
index 0000000..e77ed31
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabase.h
@@ -0,0 +1,140 @@
+/*
+ * 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 "FIRDatabaseSwiftNameSupport.h"
+
+@class FIRApp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The entry point for accessing a Firebase Database. You can get an instance by calling
+ * [FIRDatabase database]. To access a location in the database and read or write data,
+ * use [FIRDatabase reference].
+ */
+FIR_SWIFT_NAME(Database)
+@interface FIRDatabase : NSObject
+
+/**
+ * Gets the instance of FIRDatabase for the default FIRApp.
+ *
+ * @return A FIRDatabase instance.
+ */
++ (FIRDatabase *) database FIR_SWIFT_NAME(database());
+
+/**
+ * Gets an instance of FIRDatabase for a specific FIRApp.
+ *
+ * @param app The FIRApp to get a FIRDatabase for.
+ * @return A FIRDatabase instance.
+ */
++ (FIRDatabase *) databaseForApp:(FIRApp*)app FIR_SWIFT_NAME(database(app:));
+
+/** The FIRApp instance to which this FIRDatabase belongs. */
+@property (weak, readonly, nonatomic) FIRApp *app;
+
+/**
+ * Gets a FIRDatabaseReference for the root of your Firebase Database.
+ */
+- (FIRDatabaseReference *) reference;
+
+/**
+ * Gets a FIRDatabaseReference for the provided path.
+ *
+ * @param path Path to a location in your Firebase Database.
+ * @return A FIRDatabaseReference pointing to the specified path.
+ */
+- (FIRDatabaseReference *) referenceWithPath:(NSString *)path;
+
+/**
+ * Gets a FIRDatabaseReference for the provided URL. The URL must be a URL to a path
+ * within this Firebase Database. To create a FIRDatabaseReference to a different database,
+ * create a FIRApp} with a FIROptions object configured with the appropriate database URL.
+ *
+ * @param databaseUrl A URL to a path within your database.
+ * @return A FIRDatabaseReference for the provided URL.
+*/
+- (FIRDatabaseReference *) referenceFromURL:(NSString *)databaseUrl;
+
+/**
+ * The Firebase Database client automatically queues writes and sends them to the server at the earliest opportunity,
+ * depending on network connectivity. In some cases (e.g. offline usage) there may be a large number of writes
+ * waiting to be sent. Calling this method will purge all outstanding writes so they are abandoned.
+ *
+ * All writes will be purged, including transactions and onDisconnect writes. The writes will
+ * be rolled back locally, perhaps triggering events for affected event listeners, and the client will not
+ * (re-)send them to the Firebase Database backend.
+ */
+- (void)purgeOutstandingWrites;
+
+/**
+ * Shuts down our connection to the Firebase Database backend until goOnline is called.
+ */
+- (void)goOffline;
+
+/**
+ * Resumes our connection to the Firebase Database backend after a previous goOffline call.
+ */
+- (void)goOnline;
+
+/**
+ * The Firebase Database client will cache synchronized data and keep track of all writes you've
+ * initiated while your application is running. It seamlessly handles intermittent network
+ * connections and re-sends write operations when the network connection is restored.
+ *
+ * However by default your write operations and cached data are only stored in-memory and will
+ * be lost when your app restarts. By setting this value to `YES`, the data will be persisted
+ * to on-device (disk) storage and will thus be available again when the app is restarted
+ * (even when there is no network connectivity at that time). Note that this property must be
+ * set before creating your first Database reference and only needs to be called once per
+ * application.
+ *
+ */
+@property (nonatomic) BOOL persistenceEnabled FIR_SWIFT_NAME(isPersistenceEnabled);
+
+/**
+ * By default the Firebase Database client will use up to 10MB of disk space to cache data. If the cache grows beyond
+ * this size, the client will start removing data that hasn't been recently used. If you find that your application
+ * caches too little or too much data, call this method to change the cache size. This property must be set before
+ * creating your first FIRDatabaseReference and only needs to be called once per application.
+ *
+ * Note that the specified cache size is only an approximation and the size on disk may temporarily exceed it
+ * at times. Cache sizes smaller than 1 MB or greater than 100 MB are not supported.
+ */
+@property (nonatomic) NSUInteger persistenceCacheSizeBytes;
+
+/**
+ * Sets the dispatch queue on which all events are raised. The default queue is the main queue.
+ *
+ * Note that this must be set before creating your first Database reference.
+ */
+@property (nonatomic, strong) dispatch_queue_t callbackQueue;
+
+/**
+ * Enables verbose diagnostic logging.
+ *
+ * @param enabled YES to enable logging, NO to disable.
+ */
++ (void) setLoggingEnabled:(BOOL)enabled;
+
+/** Retrieve the Firebase Database SDK version. */
++ (NSString *) sdkVersion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDatabase.m b/Firebase/Database/Api/FIRDatabase.m
new file mode 100644
index 0000000..124b463
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabase.m
@@ -0,0 +1,268 @@
+/*
+ * 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 "FIRDatabase.h"
+#import "FIRDatabase_Private.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FRepoManager.h"
+#import "FValidation.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FRepoInfo.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseReference_Private.h"
+
+/**
+ * This is a hack that defines all the methods we need from FIRApp/Options. At runtime we use reflection to get the
+ * default FIRApp instance if we need it. Since protocols don't carry any runtime information and selectors
+ * are invoked by name we can write code against this protocol as long as the method signatures don't change.
+ *
+ * TODO: Consider weak-linking the actual Firebase/Core framework or something.
+ */
+
+extern NSString *const kFIRDefaultAppName;
+
+@protocol FIROptionsLike <NSObject>
+@property(nonatomic, readonly, copy) NSString *databaseURL;
+@end
+
+@protocol FIRAppLike <NSObject>
+@property(nonatomic, readonly) id<FIROptionsLike> options;
+@property(nonatomic, copy, readonly) NSString *name;
+@end
+
+@interface FIRDatabase ()
+@property (nonatomic, strong) FRepoInfo *repoInfo;
+@property (nonatomic, strong) FIRDatabaseConfig *config;
+@property (nonatomic, strong) FRepo *repo;
+@end
+
+@implementation FIRDatabase
+
+// The STR and STR_EXPAND macro allow a numeric version passed to he compiler driver
+// with a -D to be treated as a string instead of an invalid floating point value.
+#define STR(x) STR_EXPAND(x)
+#define STR_EXPAND(x) #x
+static const char *FIREBASE_SEMVER = (const char *)STR(FIRDatabase_VERSION);
+
+/**
+ * A static NSMutableDictionary of FirebaseApp names to FirebaseDatabase instance. To ensure thread-
+ * safety, it should only be accessed in databaseForApp, which is synchronized.
+ *
+ * TODO: This serves a duplicate purpose as RepoManager. We should clean up.
+ * TODO: We should maybe be conscious of leaks and make this a weak map or similar
+ * but we have a lot of work to do to allow FirebaseDatabase/Repo etc. to be GC'd.
+ */
++ (NSMutableDictionary *)instances {
+ static dispatch_once_t pred = 0;
+ static NSMutableDictionary *instances;
+ dispatch_once(&pred, ^{
+ instances = [NSMutableDictionary dictionary];
+ });
+ return instances;
+}
+
++ (FIRDatabase *)database {
+ id<FIRAppLike> app = [FIRDatabase getDefaultApp];
+ if (app == nil) {
+ [NSException raise:@"FIRAppNotConfigured" format:@"Failed to get default FIRDatabase instance. Must call FIRApp.configure() before using FIRDatabase."];
+ }
+ return [FIRDatabase databaseForApp:(FIRApp*)app];
+}
+
++ (FIRDatabase *)databaseForApp:(id)app {
+ if (app == nil) {
+ [NSException raise:@"InvalidFIRApp" format:@"nil FIRApp instance passed to databaseForApp."];
+ }
+ NSMutableDictionary *instances = [self instances];
+ @synchronized (instances) {
+ id<FIRAppLike> appLike = (id<FIRAppLike>)app;
+ FIRDatabase *database = instances[appLike.name];
+ if (!database) {
+ NSString *databaseUrl = appLike.options.databaseURL;
+ if (databaseUrl == nil) {
+ [NSException raise:@"MissingDatabaseURL" format:@"Failed to get FIRDatabase instance: FIRApp object has no "
+ "databaseURL in its FirebaseOptions object."];
+ }
+
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:databaseUrl];
+ if (![parsedUrl.path isEmpty]) {
+ [NSException raise:@"InvalidDatabaseURL" format:@"Configured Database URL '%@' is invalid. It should "
+ "point to the root of a Firebase Database but it includes a path: %@",
+ databaseUrl, [parsedUrl.path toString]];
+ }
+
+ id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:appLike];
+
+ // If this is the default app, don't set the session persistence key so that we use our
+ // default ("default") instead of the FIRApp default ("[DEFAULT]") so that we
+ // preserve the default location used by the legacy Firebase SDK.
+ NSString *sessionIdentifier = @"default";
+ if (![appLike.name isEqualToString:kFIRDefaultAppName]) {
+ sessionIdentifier = appLike.name;
+ }
+
+ FIRDatabaseConfig *config = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:sessionIdentifier
+ authTokenProvider:authTokenProvider];
+ database = [[FIRDatabase alloc] initWithApp:appLike repoInfo:parsedUrl.repoInfo config:config];
+ instances[appLike.name] = database;
+ }
+
+ return database;
+ }
+}
+
++ (NSString *) buildVersion {
+ // TODO: Restore git hash when build moves back to git
+ return [NSString stringWithFormat:@"%s_%s", FIREBASE_SEMVER, __DATE__];
+}
+
++ (FIRDatabase *)createDatabaseForTests:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config {
+ FIRDatabase *db = [[FIRDatabase alloc] initWithApp:nil repoInfo:repoInfo config:config];
+ [db ensureRepo];
+ return db;
+}
+
+
++ (NSString *) sdkVersion {
+ return [NSString stringWithUTF8String:FIREBASE_SEMVER];
+}
+
++ (void) setLoggingEnabled:(BOOL)enabled {
+ [FUtilities setLoggingEnabled:enabled];
+ FFLog(@"I-RDB024001", @"BUILD Version: %@", [FIRDatabase buildVersion]);
+}
+
+
+- (id)initWithApp:(id <FIRAppLike>)appLike repoInfo:(FRepoInfo *)info config:(FIRDatabaseConfig *)config {
+ self = [super init];
+ if (self != nil) {
+ self->_repoInfo = info;
+ self->_config = config;
+ self->_app = (FIRApp*) appLike;
+ }
+ return self;
+}
+
+- (FIRDatabaseReference *)reference {
+ [self ensureRepo];
+
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:[FPath empty]];
+}
+
+- (FIRDatabaseReference *)referenceWithPath:(NSString *)path {
+ [self ensureRepo];
+
+ [FValidation validateFrom:@"referenceWithPath" validRootPathString:path];
+ FPath *childPath = [[FPath alloc] initWith:path];
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:childPath];
+}
+
+- (FIRDatabaseReference *)referenceFromURL:(NSString *)databaseUrl {
+ [self ensureRepo];
+
+ if (databaseUrl == nil) {
+ [NSException raise:@"InvalidDatabaseURL" format:@"Invalid nil url passed to referenceFromURL:"];
+ }
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:databaseUrl];
+ [FValidation validateFrom:@"referenceFromURL:" validURL:parsedUrl];
+ if (![parsedUrl.repoInfo.host isEqualToString:_repoInfo.host]) {
+ [NSException raise:@"InvalidDatabaseURL" format:@"Invalid URL (%@) passed to getReference(). URL was expected "
+ "to match configured Database URL: %@", databaseUrl, [self reference].URL];
+ }
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:parsedUrl.path];
+}
+
+
+- (void)purgeOutstandingWrites {
+ [self ensureRepo];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo purgeOutstandingWrites];
+ });
+}
+
+- (void)goOnline {
+ [self ensureRepo];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo resume];
+ });
+}
+
+
+- (void)goOffline {
+ [self ensureRepo];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo interrupt];
+ });
+}
+
++ (id<FIRAppLike>) getDefaultApp {
+ Class appClass = NSClassFromString(@"FIRApp");
+ if (appClass == nil) {
+ [NSException raise:@"FailedToFindFIRApp" format:@"Failed to find FIRApp class."];
+ return nil;
+ } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+ return [appClass performSelector:@selector(defaultApp)];
+#pragma clang diagnostic pop
+ }
+}
+
+- (void)setPersistenceEnabled:(BOOL)persistenceEnabled {
+ [self assertUnfrozen:@"setPersistenceEnabled"];
+ self->_config.persistenceEnabled = persistenceEnabled;
+}
+
+- (BOOL)persistenceEnabled {
+ return self->_config.persistenceEnabled;
+}
+
+- (void)setPersistenceCacheSizeBytes:(NSUInteger)persistenceCacheSizeBytes {
+ [self assertUnfrozen:@"setPersistenceCacheSizeBytes"];
+ self->_config.persistenceCacheSizeBytes = persistenceCacheSizeBytes;
+}
+
+- (NSUInteger)persistenceCacheSizeBytes {
+ return self->_config.persistenceCacheSizeBytes;
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue {
+ [self assertUnfrozen:@"setCallbackQueue"];
+ self->_config.callbackQueue = callbackQueue;
+}
+
+- (dispatch_queue_t)callbackQueue {
+ return self->_config.callbackQueue;
+}
+
+- (void) assertUnfrozen:(NSString*)methodName {
+ if (self.repo != nil) {
+ [NSException raise:@"FIRDatabaseAlreadyInUse" format:@"Calls to %@ must be made before any other usage of "
+ "FIRDatabase instance.", methodName];
+ }
+}
+
+- (void) ensureRepo {
+ if (self.repo == nil) {
+ self.repo = [FRepoManager createRepo:self.repoInfo config:self.config database:self];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabaseConfig.h b/Firebase/Database/Api/FIRDatabaseConfig.h
new file mode 100644
index 0000000..d41f3a8
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseConfig.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>
+
+@protocol FAuthTokenProvider;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * TODO: Merge FIRDatabaseConfig into FIRDatabase.
+ */
+@interface FIRDatabaseConfig : NSObject
+
+- (id)initWithSessionIdentifier:(NSString *)identifier authTokenProvider:(id<FAuthTokenProvider>)authTokenProvider;
+
+/**
+ * By default the Firebase Database client will keep data in memory while your application is running, but not
+ * when it is restarted. By setting this value to YES, the data will be persisted to on-device (disk)
+ * storage and will thus be available again when the app is restarted (even when there is no network
+ * connectivity at that time). Note that this property must be set before creating your first FIRDatabaseReference
+ * and only needs to be called once per application.
+ *
+ * If your app uses Firebase Authentication, the client will automatically persist the user's authentication
+ * token across restarts, even without persistence enabled. But if the auth token expired while offline and
+ * you've enabled persistence, the client will pause write operations until you successfully re-authenticate
+ * (or explicitly unauthenticate) to prevent your writes from being sent unauthenticated and failing due to
+ * security rules.
+ */
+@property (nonatomic) BOOL persistenceEnabled;
+
+/**
+ * By default the Firebase Database client will use up to 10MB of disk space to cache data. If the cache grows beyond this size,
+ * the client will start removing data that hasn't been recently used. If you find that your application caches too
+ * little or too much data, call this method to change the cache size. This property must be set before creating
+ * your first FIRDatabaseReference and only needs to be called once per application.
+ *
+ * Note that the specified cache size is only an approximation and the size on disk may temporarily exceed it
+ * at times.
+ */
+@property (nonatomic) NSUInteger persistenceCacheSizeBytes;
+
+/**
+ * Sets the dispatch queue on which all events are raised. The default queue is the main queue.
+ */
+@property (nonatomic, strong) dispatch_queue_t callbackQueue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDatabaseConfig.m b/Firebase/Database/Api/FIRDatabaseConfig.m
new file mode 100644
index 0000000..f4639f9
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseConfig.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 "FIRApp.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FIRNoopAuthTokenProvider.h"
+#import "FAuthTokenProvider.h"
+
+@interface FIRDatabaseConfig (Private)
+
+@property (nonatomic, strong, readwrite) NSString *sessionIdentifier;
+
+@end
+
+@implementation FIRDatabaseConfig
+
+- (id)init {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create config objects!"];
+ return nil;
+}
+
+- (id)initWithSessionIdentifier:(NSString *)identifier authTokenProvider:(id<FAuthTokenProvider>)authTokenProvider {
+ self = [super init];
+ if (self != nil) {
+ self->_sessionIdentifier = identifier;
+ self->_callbackQueue = dispatch_get_main_queue();
+ self->_persistenceCacheSizeBytes = 10*1024*1024; // Default cache size is 10MB
+ self->_authTokenProvider = authTokenProvider;
+ }
+ return self;
+}
+
+- (void)assertUnfrozen {
+ if (self.isFrozen) {
+ [NSException raise:NSGenericException format:@"Can't modify config objects after they are in use for FIRDatabaseReferences."];
+ }
+}
+
+- (void)setAuthTokenProvider:(id<FAuthTokenProvider>)authTokenProvider {
+ [self assertUnfrozen];
+ self->_authTokenProvider = authTokenProvider;
+}
+
+- (void)setPersistenceEnabled:(BOOL)persistenceEnabled {
+ [self assertUnfrozen];
+ self->_persistenceEnabled = persistenceEnabled;
+}
+
+- (void)setPersistenceCacheSizeBytes:(NSUInteger)persistenceCacheSizeBytes {
+ [self assertUnfrozen];
+ // Can't be less than 1MB
+ if (persistenceCacheSizeBytes < 1024*1024) {
+ [NSException raise:NSInvalidArgumentException format:@"The minimum cache size must be at least 1MB"];
+ }
+ if (persistenceCacheSizeBytes > 100*1024*1024) {
+ [NSException raise:NSInvalidArgumentException format:@"Firebase Database currently doesn't support a cache size larger than 100MB"];
+ }
+ self->_persistenceCacheSizeBytes = persistenceCacheSizeBytes;
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue {
+ [self assertUnfrozen];
+ self->_callbackQueue = callbackQueue;
+}
+
+- (void)freeze {
+ self->_isFrozen = YES;
+}
+
+// TODO: Only used for tests. Migrate to FIRDatabase and remove.
++ (FIRDatabaseConfig *)defaultConfig {
+ static dispatch_once_t onceToken;
+ static FIRDatabaseConfig *defaultConfig;
+ dispatch_once(&onceToken, ^{
+ defaultConfig = [FIRDatabaseConfig configForName:@"default"];
+ });
+ return defaultConfig;
+}
+
+// TODO: This is only used for tests. We should fix them to go through FIRDatabase and remove
+// this method and the sessionsConfigs dictionary (FIRDatabase automatically creates one config per app).
++ (FIRDatabaseConfig *)configForName:(NSString *)name {
+ NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z0-9-_]+$" options:0 error:nil];
+ if ([expression numberOfMatchesInString:name options:0 range:NSMakeRange(0, name.length)] == 0) {
+ [NSException raise:NSInvalidArgumentException format:@"Name can only contain [a-zA-Z0-9-_]"];
+ }
+
+ static dispatch_once_t onceToken;
+ static NSMutableDictionary *sessionConfigs;
+ dispatch_once(&onceToken, ^{
+ sessionConfigs = [NSMutableDictionary dictionary];
+ });
+ @synchronized(sessionConfigs) {
+ if (!sessionConfigs[name]) {
+ id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:[FIRApp defaultApp]];
+ sessionConfigs[name] = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:name
+ authTokenProvider:authTokenProvider];
+ }
+ return sessionConfigs[name];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabaseQuery.h b/Firebase/Database/Api/FIRDatabaseQuery.h
new file mode 100644
index 0000000..be4ad27
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseQuery.h
@@ -0,0 +1,315 @@
+/*
+ * 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 "FIRDatabaseSwiftNameSupport.h"
+#import "FIRDataEventType.h"
+#import "FIRDataSnapshot.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A FIRDatabaseHandle is used to identify listeners of Firebase Database events. These handles
+ * are returned by observeEventType: and and can later be passed to removeObserverWithHandle: to
+ * stop receiving updates.
+ */
+typedef NSUInteger FIRDatabaseHandle FIR_SWIFT_NAME(DatabaseHandle);
+
+/**
+ * A FIRDatabaseQuery instance represents a query over the data at a particular location.
+ *
+ * You create one by calling one of the query methods (queryOrderedByChild:, queryStartingAtValue:, etc.)
+ * on a FIRDatabaseReference. The query methods can be chained to further specify the data you are interested in
+ * observing
+ */
+FIR_SWIFT_NAME(DatabaseQuery)
+@interface FIRDatabaseQuery : NSObject
+
+
+#pragma mark - Attach observers to read data
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+#pragma mark - Detaching observers
+
+/**
+ * Detach a block previously attached with observeEventType:withBlock:.
+ *
+ * @param handle The handle returned by the call to observeEventType:withBlock: which we are trying to remove.
+ */
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle;
+
+
+/**
+ * Detach all blocks previously attached to this Firebase Database location with observeEventType:withBlock:
+ */
+- (void) removeAllObservers;
+
+/**
+ * By calling `keepSynced:YES` on a location, the data for that location will automatically be downloaded and
+ * kept in sync, even when no listeners are attached for that location. Additionally, while a location is kept
+ * synced, it will not be evicted from the persistent disk cache.
+ *
+ * @param keepSynced Pass YES to keep this location synchronized, pass NO to stop synchronization.
+*/
+ - (void) keepSynced:(BOOL)keepSynced;
+
+
+#pragma mark - Querying and limiting
+
+/**
+* queryLimitedToFirst: is used to generate a reference to a limited view of the data at this location.
+* The FIRDatabaseQuery instance returned by queryLimitedToFirst: will respond to at most the first limit child nodes.
+*
+* @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+* @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+*/
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit;
+
+
+/**
+* queryLimitedToLast: is used to generate a reference to a limited view of the data at this location.
+* The FIRDatabaseQuery instance returned by queryLimitedToLast: will respond to at most the last limit child nodes.
+*
+* @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+* @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+*/
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit;
+
+/**
+ * queryOrderBy: is used to generate a reference to a view of the data that's been sorted by the values of
+ * a particular child key. This method is intended to be used in combination with queryStartingAtValue:,
+ * queryEndingAtValue:, or queryEqualToValue:.
+ *
+ * @param key The child key to use in ordering data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, ordered by the values of the specified child key.
+*/
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)key;
+
+/**
+ * queryOrderedByKey: is used to generate a reference to a view of the data that's been sorted by child key.
+ * This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child keys.
+ */
+- (FIRDatabaseQuery *) queryOrderedByKey;
+
+/**
+ * queryOrderedByValue: is used to generate a reference to a view of the data that's been sorted by child value.
+ * This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child value.
+ */
+- (FIRDatabaseQuery *) queryOrderedByValue;
+
+/**
+ * queryOrderedByPriority: is used to generate a reference to a view of the data that's been sorted by child
+ * priority. This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child priorities.
+ */
+- (FIRDatabaseQuery *) queryOrderedByPriority;
+
+/**
+ * queryStartingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue: will respond to events at nodes with a value
+ * greater than or equal to startValue.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue;
+
+/**
+ * queryStartingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue:childKey will respond to events at nodes with a value
+ * greater than startValue, or equal to startValue and with a key greater than or equal to childKey. This is most
+ * useful when implementing pagination in a case where multiple nodes can match the startValue.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The lower bound, inclusive, for the key of nodes with value equal to startValue
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEndingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue: will respond to events at nodes with a value
+ * less than or equal to endValue.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue;
+
+/**
+ * queryEndingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue:childKey will respond to events at nodes with a value
+ * less than endValue, or equal to endValue and with a key less than or equal to childKey. This is most useful when
+ * implementing pagination in a case where multiple nodes can match the endValue.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The upper bound, inclusive, for the key of nodes with value equal to endValue
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEqualToValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue: will respond to events at nodes with a value equal
+ * to the supplied argument.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value;
+
+/**
+ * queryEqualToValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue:childKey will respond to events at nodes with a value
+ * equal to the supplied argument and with their key equal to childKey. There will be at most one node that matches
+ * because child keys are unique.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @param childKey The name of nodes with the right value
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value and the key.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value childKey:(nullable NSString *)childKey;
+
+
+#pragma mark - Properties
+
+/**
+* Gets a FIRDatabaseReference for the location of this query.
+*
+* @return A FIRDatabaseReference for the location of this query.
+*/
+@property (nonatomic, readonly, strong) FIRDatabaseReference * ref;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRDatabaseQuery.m b/Firebase/Database/Api/FIRDatabaseQuery.m
new file mode 100644
index 0000000..bcb1733
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseQuery.m
@@ -0,0 +1,525 @@
+/*
+ * 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 "FIRDatabaseQuery.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FValidation.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FValueEventRegistration.h"
+#import "FChildEventRegistration.h"
+#import "FPath.h"
+#import "FKeyIndex.h"
+#import "FPathIndex.h"
+#import "FPriorityIndex.h"
+#import "FValueIndex.h"
+#import "FLeafNode.h"
+#import "FSnapshotUtilities.h"
+#import "FConstants.h"
+
+@implementation FIRDatabaseQuery
+
+@synthesize repo;
+@synthesize path;
+@synthesize queryParams;
+
+#define INVALID_QUERY_PARAM_ERROR @"InvalidQueryParameter"
+
+
++ (dispatch_queue_t)sharedQueue
+{
+ // We use this shared queue across all of the FQueries so things happen FIFO (as opposed to dispatch_get_global_queue(0, 0) which is concurrent)
+ static dispatch_once_t pred;
+ static dispatch_queue_t sharedDispatchQueue;
+
+ dispatch_once(&pred, ^{
+ sharedDispatchQueue = dispatch_queue_create("FirebaseWorker", NULL);
+ });
+
+ return sharedDispatchQueue;
+}
+
+- (id) initWithRepo:(FRepo *)theRepo path:(FPath *)thePath {
+ return [self initWithRepo:theRepo path:thePath params:nil orderByCalled:NO priorityMethodCalled:NO];
+}
+
+- (id) initWithRepo:(FRepo *)theRepo
+ path:(FPath *)thePath
+ params:(FQueryParams *)theParams
+ orderByCalled:(BOOL)orderByCalled
+priorityMethodCalled:(BOOL)priorityMethodCalled {
+ self = [super init];
+ if (self) {
+ self.repo = theRepo;
+ self.path = thePath;
+ if (!theParams) {
+ theParams = [FQueryParams defaultInstance];
+ }
+ if (![theParams isValid]) {
+ @throw [[NSException alloc] initWithName:@"InvalidArgumentError" reason:@"Queries are limited to two constraints" userInfo:nil];
+ }
+ self.queryParams = theParams;
+ self.orderByCalled = orderByCalled;
+ self.priorityMethodCalled = priorityMethodCalled;
+ }
+ return self;
+}
+
+- (FQuerySpec *)querySpec {
+ return [[FQuerySpec alloc] initWithPath:self.path params:self.queryParams];
+}
+
+- (void)validateQueryEndpointsForParams:(FQueryParams *)params {
+ if ([params.index isEqual:[FKeyIndex keyIndex]]) {
+ if ([params hasStart]) {
+ if (params.indexStartKey != [FUtilities minName]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue:childKey: or queryEqualTo:andChildKey: in combination with queryOrderedByKey"];
+ }
+ if (![params.indexStartValue.val isKindOfClass:[NSString class]]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue: with other types than string in combination with queryOrderedByKey"];
+ }
+ }
+ if ([params hasEnd]) {
+ if (params.indexEndKey != [FUtilities maxName]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue:childKey: or queryEqualToValue:childKey: in combination with queryOrderedByKey"];
+ }
+ if (![params.indexEndValue.val isKindOfClass:[NSString class]]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue: with other types than string in combination with queryOrderedByKey"];
+ }
+ }
+ } else if ([params.index isEqual:[FPriorityIndex priorityIndex]]) {
+ if (([params hasStart] && ![FValidation validatePriorityValue:params.indexStartValue.val]) ||
+ ([params hasEnd] && ![FValidation validatePriorityValue:params.indexEndValue.val])) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"When using queryOrderedByPriority, values provided to queryStartingAtValue:, queryEndingAtValue:, or queryEqualToValue: must be valid priorities."];
+ }
+ }
+}
+
+- (void)validateEqualToCall {
+ if ([self.queryParams hasStart]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryStartingAtValue:"];
+ }
+ if ([self.queryParams hasEnd]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryEndingAtValue:"];
+ }
+}
+
+- (void)validateNoPreviousOrderByCalled {
+ if (self.orderByCalled) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot use multiple queryOrderedBy calls!"];
+ }
+}
+
+- (void)validateIndexValueType:(id)type fromMethod:(NSString *)method {
+ if (type != nil &&
+ ![type isKindOfClass:[NSNumber class]] &&
+ ![type isKindOfClass:[NSString class]] &&
+ ![type isKindOfClass:[NSNull class]]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"You can only pass nil, NSString or NSNumber to %@", method];
+ }
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue {
+ return [self queryStartingAtInternal:startValue childKey:nil from:@"queryStartingAtValue:" priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue childKey:(NSString *)childKey {
+ if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:@"You must use queryStartingAtValue: instead of queryStartingAtValue:childKey: when using queryOrderedByKey:"
+ userInfo:nil];
+ }
+ return [self queryStartingAtInternal:startValue
+ childKey:childKey
+ from:@"queryStartingAtValue:childKey:"
+ priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtInternal:(id<FNode>)startValue
+ childKey:(NSString *)childKey
+ from:(NSString *)methodName
+ priorityMethod:(BOOL)priorityMethod {
+ [self validateIndexValueType:startValue fromMethod:methodName];
+ if (childKey != nil) {
+ [FValidation validateFrom:methodName validKey:childKey];
+ }
+ if ([self.queryParams hasStart]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR
+ format:@"Can't call %@ after queryStartingAtValue or queryEqualToValue was previously called", methodName];
+ }
+ id<FNode> startNode = [FSnapshotUtilities nodeFrom:startValue];
+ FQueryParams* params = [self.queryParams startAt:startNode childKey:childKey];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
+ return [self queryEndingAtInternal:endValue
+ childKey:nil
+ from:@"queryEndingAtValue:"
+ priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue childKey:(NSString *)childKey {
+ if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:@"You must use queryEndingAtValue: instead of queryEndingAtValue:childKey: when using queryOrderedByKey:"
+ userInfo:nil];
+ }
+
+ return [self queryEndingAtInternal:endValue
+ childKey:childKey
+ from:@"queryEndingAtValue:childKey:"
+ priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtInternal:(id)endValue
+ childKey:(NSString *)childKey
+ from:(NSString *)methodName
+ priorityMethod:(BOOL)priorityMethod {
+ [self validateIndexValueType:endValue fromMethod:methodName];
+ if (childKey != nil) {
+ [FValidation validateFrom:methodName validKey:childKey];
+ }
+ if ([self.queryParams hasEnd]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR
+ format:@"Can't call %@ after queryEndingAtValue or queryEqualToValue was previously called", methodName];
+ }
+ id<FNode> endNode = [FSnapshotUtilities nodeFrom:endValue];
+ FQueryParams* params = [self.queryParams endAt:endNode childKey:childKey];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value {
+ return [self queryEqualToInternal:value childKey:nil from:@"queryEqualToValue:" priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value childKey:(NSString *)childKey {
+ if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:@"You must use queryEqualToValue: instead of queryEqualTo:childKey: when using queryOrderedByKey:"
+ userInfo:nil];
+ }
+ return [self queryEqualToInternal:value childKey:childKey from:@"queryEqualToValue:childKey:" priorityMethod:NO];
+}
+
+- (FIRDatabaseQuery *)queryEqualToInternal:(id)value
+ childKey:(NSString *)childKey
+ from:(NSString *)methodName
+ priorityMethod:(BOOL)priorityMethod {
+ [self validateIndexValueType:value fromMethod:methodName];
+ if (childKey != nil) {
+ [FValidation validateFrom:methodName validKey:childKey];
+ }
+ if ([self.queryParams hasEnd] || [self.queryParams hasStart]) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR
+ format:@"Can't call %@ after queryStartingAtValue, queryEndingAtValue or queryEqualToValue was previously called", methodName];
+ }
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value];
+ FQueryParams* params = [[self.queryParams startAt:node childKey:childKey] endAt:node childKey:childKey];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
+}
+
+- (void)validateLimitRange:(NSUInteger)limit
+{
+ // No need to check for negative ranges, since limit is unsigned
+ if (limit == 0) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit can't be zero"];
+ }
+ if (limit >= 1l<<31) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit must be less than 2,147,483,648"];
+ }
+}
+
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit {
+ if (self.queryParams.limitSet) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToFirst: if a limit was previously set"];
+ }
+ [self validateLimitRange:limit];
+ FQueryParams* params = [self.queryParams limitToFirst:limit];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit {
+ if (self.queryParams.limitSet) {
+ [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToLast: if a limit was previously set"];
+ }
+ [self validateLimitRange:limit];
+ FQueryParams* params = [self.queryParams limitToLast:limit];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:self.orderByCalled
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)indexPathString {
+ if ([indexPathString isEqualToString:@"$key"] || [indexPathString isEqualToString:@".key"]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByKey: instead.", indexPathString]
+ userInfo:nil];
+ } else if ([indexPathString isEqualToString:@"$priority"] || [indexPathString isEqualToString:@".priority"]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByPriority: instead.", indexPathString]
+ userInfo:nil];
+ } else if ([indexPathString isEqualToString:@"$value"] || [indexPathString isEqualToString:@".value"]) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByValue: instead.", indexPathString]
+ userInfo:nil];
+ }
+ [self validateNoPreviousOrderByCalled];
+
+ [FValidation validateFrom:@"queryOrderedByChild:" validPathString:indexPathString];
+ FPath *indexPath = [FPath pathWithString:indexPathString];
+ if (indexPath.isEmpty) {
+ @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
+ reason:[NSString stringWithFormat:@"(queryOrderedByChild:) with an empty path is invalid. Use queryOrderedByValue: instead."]
+ userInfo:nil];
+ }
+ id<FIndex> index = [[FPathIndex alloc] initWithPath:indexPath];
+
+ FQueryParams *params = [self.queryParams orderBy:index];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByKey {
+ [self validateNoPreviousOrderByCalled];
+ FQueryParams *params = [self.queryParams orderBy:[FKeyIndex keyIndex]];
+ [self validateQueryEndpointsForParams:params];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByValue {
+ [self validateNoPreviousOrderByCalled];
+ FQueryParams *params = [self.queryParams orderBy:[FValueIndex valueIndex]];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByPriority {
+ [self validateNoPreviousOrderByCalled];
+ FQueryParams *params = [self.queryParams orderBy:[FPriorityIndex priorityIndex]];
+ return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
+ path:self.path
+ params:params
+ orderByCalled:YES
+ priorityMethodCalled:self.priorityMethodCalled];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *))block {
+ [FValidation validateFrom:@"observeEventType:withBlock:" knownEventType:eventType];
+ return [self observeEventType:eventType withBlock:block withCancelBlock:nil];
+}
+
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+ [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:" knownEventType:eventType];
+ return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [FValidation validateFrom:@"observeEventType:withBlock:withCancelBlock:" knownEventType:eventType];
+
+ if (eventType == FIRDataEventTypeValue) {
+ // Handle FIRDataEventTypeValue specially because they shouldn't have prevName callbacks
+ NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
+ [self observeValueEventWithHandle:handle withBlock:block cancelCallback:cancelBlock];
+ return handle;
+ } else {
+ // Wrap up the userCallback so we can treat everything as a callback that has a prevName
+ fbt_void_datasnapshot userCallback = [block copy];
+ return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ if (userCallback != nil) {
+ userCallback(snapshot);
+ }
+ } withCancelBlock:cancelBlock];
+ }
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:" knownEventType:eventType];
+
+
+ if (eventType == FIRDataEventTypeValue) {
+ // TODO: This gets hit by observeSingleEventOfType. Need to fix.
+ /*
+ @throw [[NSException alloc] initWithName:@"InvalidEventTypeForObserver"
+ reason:@"(observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:) Cannot use observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock: with FIRDataEventTypeValue. Use observeEventType:withBlock:withCancelBlock: instead."
+ userInfo:nil];
+ */
+ }
+
+ NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
+ NSDictionary *callbacks = @{[NSNumber numberWithInteger:eventType]: [block copy]};
+ [self observeChildEventWithHandle:handle withCallbacks:callbacks cancelCallback:cancelBlock];
+
+ return handle;
+}
+
+// If we want to distinguish between value event listeners and child event listeners, like in the Java client, we can
+// consider exporting this. If we do, add argument validation. Otherwise, arguments are validated in the public-facing
+// portions of the API. Also, move the FIRDatabaseHandle logic.
+- (void)observeValueEventWithHandle:(FIRDatabaseHandle)handle withBlock:(fbt_void_datasnapshot)block cancelCallback:(fbt_void_nserror)cancelBlock {
+ // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
+ FValueEventRegistration *registration = [[FValueEventRegistration alloc] initWithRepo:self.repo
+ handle:handle
+ callback:block
+ cancelCallback:cancelBlock];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo addEventRegistration:registration forQuery:self.querySpec];
+ });
+}
+
+// Note: as with the above method, we may wish to expose this at some point.
+- (void)observeChildEventWithHandle:(FIRDatabaseHandle)handle withCallbacks:(NSDictionary *)callbacks cancelCallback:(fbt_void_nserror)cancelBlock {
+ // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
+ FChildEventRegistration *registration = [[FChildEventRegistration alloc] initWithRepo:self.repo
+ handle:handle
+ callbacks:callbacks
+ cancelCallback:cancelBlock];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo addEventRegistration:registration forQuery:self.querySpec];
+ });
+}
+
+
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle {
+ FValueEventRegistration *event = [[FValueEventRegistration alloc] initWithRepo:self.repo
+ handle:handle
+ callback:nil
+ cancelCallback:nil];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo removeEventRegistration:event forQuery:self.querySpec];
+ });
+}
+
+
+- (void) removeAllObservers {
+ [self removeObserverWithHandle:NSNotFound];
+}
+
+- (void)keepSynced:(BOOL)keepSynced {
+ if ([self.path.getFront isEqualToString:kDotInfoPrefix]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't keep query on .info tree synced (this already is the case)."];
+ }
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo keepQuery:self.querySpec synced:keepSynced];
+ });
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
+
+ [self observeSingleEventOfType:eventType withBlock:block withCancelBlock:nil];
+}
+
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+
+ [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+
+ // XXX: user reported memory leak in method
+
+ // "When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top). If you have block variables and you reference a block from within the block, that block will be copied."
+ // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
+ // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
+ // __block fbt_void_datasnapshot userCallback = [callback copy];
+
+ [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ if (block != nil) {
+ block(snapshot);
+ }
+ } withCancelBlock:cancelBlock];
+}
+
+/**
+* Attaches a listener, waits for the first event, and then removes the listener
+*/
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+
+ // XXX: user reported memory leak in method
+
+ // "When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top). If you have block variables and you reference a block from within the block, that block will be copied."
+ // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
+ // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
+ // __block fbt_void_datasnapshot userCallback = [callback copy];
+
+ __block FIRDatabaseHandle handle;
+ __block BOOL firstCall = YES;
+
+ fbt_void_datasnapshot_nsstring callback = [block copy];
+ fbt_void_datasnapshot_nsstring wrappedCallback = ^(FIRDataSnapshot *snap, NSString* prevName) {
+ if (firstCall) {
+ firstCall = NO;
+ [self removeObserverWithHandle:handle];
+ callback(snap, prevName);
+ }
+ };
+
+ fbt_void_nserror cancelCallback = [cancelBlock copy];
+ handle = [self observeEventType:eventType andPreviousSiblingKeyWithBlock:wrappedCallback withCancelBlock:^(NSError* error){
+
+ [self removeObserverWithHandle:handle];
+
+ if (cancelCallback) {
+ cancelCallback(error);
+ }
+ }];
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"(%@ %@)", self.path, self.queryParams.description];
+}
+
+- (FIRDatabaseReference *) ref {
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:self.path];
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h b/Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h
new file mode 100644
index 0000000..529adf4
--- /dev/null
+++ b/Firebase/Database/Api/FIRDatabaseSwiftNameSupport.h
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+#ifndef FIR_SWIFT_NAME
+
+#import <Foundation/Foundation.h>
+
+// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK.
+// // Wrap it in our own macro if it's a non-compatible SDK.
+#ifdef __IPHONE_9_3
+#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X)
+#else
+#define FIR_SWIFT_NAME(X) // Intentionally blank.
+#endif // #ifdef __IPHONE_9_3
+
+#endif // FIR_SWIFT_NAME \ No newline at end of file
diff --git a/Firebase/Database/Api/FIRMutableData.h b/Firebase/Database/Api/FIRMutableData.h
new file mode 100644
index 0000000..5c26024
--- /dev/null
+++ b/Firebase/Database/Api/FIRMutableData.h
@@ -0,0 +1,130 @@
+/*
+ * 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 "FIRDatabaseSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A FIRMutableData instance is populated with data from a Firebase Database location.
+ * When you are using runTransactionBlock:, you will be given an instance containing the current
+ * data at that location. Your block will be responsible for updating that instance to the data
+ * you wish to save at that location, and then returning using [FIRTransactionResult successWithValue:].
+ *
+ * To modify the data, set its value property to any of the native types support by Firebase Database:
+ *
+ * + NSNumber (includes BOOL)
+ * + NSDictionary
+ * + NSArray
+ * + NSString
+ * + nil / NSNull to remove the data
+ *
+ * Note that changes made to a child FIRMutableData instance will be visible to the parent.
+ */
+FIR_SWIFT_NAME(MutableData)
+@interface FIRMutableData : NSObject
+
+
+#pragma mark - Inspecting and navigating the data
+
+
+/**
+ * Returns boolean indicating whether this mutable data has children.
+ *
+ * @return YES if this data contains child nodes.
+ */
+- (BOOL) hasChildren;
+
+
+/**
+ * Indicates whether this mutable data has a child at the given path.
+ *
+ * @param path A path string, consisting either of a single segment, like 'child', or multiple segments, 'a/deeper/child'
+ * @return YES if this data contains a child at the specified relative path
+ */
+- (BOOL) hasChildAtPath:(NSString *)path;
+
+
+/**
+ * Used to obtain a FIRMutableData instance that encapsulates the data at the given relative path.
+ * Note that changes made to the child will be visible to the parent.
+ *
+ * @param path A path string, consisting either of a single segment, like 'child', or multiple segments, 'a/deeper/child'
+ * @return A FIRMutableData instance containing the data at the given path
+ */
+- (FIRMutableData *)childDataByAppendingPath:(NSString *)path;
+
+
+#pragma mark - Properties
+
+
+/**
+ * To modify the data contained by this instance of FIRMutableData, set this to any of the native types supported by Firebase Database:
+ *
+ * + NSNumber (includes BOOL)
+ * + NSDictionary
+ * + NSArray
+ * + NSString
+ * + nil / NSNull to remove the data
+ *
+ * Note that setting this value will override the priority at this location.
+ *
+ * @return The current data at this location as a native object
+ */
+@property (strong, nonatomic, nullable) id value;
+
+
+/**
+ * Set this property to update the priority of the data at this location. Can be set to the following types:
+ *
+ * + NSNumber
+ * + NSString
+ * + nil / NSNull to remove the priority
+ *
+ * @return The priority of the data at this location
+ */
+@property (strong, nonatomic, nullable) id priority;
+
+
+/**
+ * @return The number of child nodes at this location
+ */
+@property (readonly, nonatomic) NSUInteger childrenCount;
+
+
+/**
+ * Used to iterate over the children at this location. You can use the native for .. in syntax:
+ *
+ * for (FIRMutableData* child in data.children) {
+ * ...
+ * }
+ *
+ * Note that this enumerator operates on an immutable copy of the child list. So, you can modify the instance
+ * during iteration, but the new additions will not be visible until you get a new enumerator.
+ */
+@property (readonly, nonatomic, strong) NSEnumerator* children;
+
+
+/**
+ * @return The key name of this node, or nil if it is the top-most location
+ */
+@property (readonly, nonatomic, strong, nullable) NSString* key;
+
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRMutableData.m b/Firebase/Database/Api/FIRMutableData.m
new file mode 100644
index 0000000..7e10dcd
--- /dev/null
+++ b/Firebase/Database/Api/FIRMutableData.m
@@ -0,0 +1,134 @@
+/*
+ * 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 "FIRMutableData.h"
+#import "FIRMutableData_Private.h"
+#import "FSnapshotHolder.h"
+#import "FSnapshotUtilities.h"
+#import "FChildrenNode.h"
+#import "FTransformedEnumerator.h"
+#import "FNamedNode.h"
+#import "FIndexedNode.h"
+
+@interface FIRMutableData ()
+
+- (id) initWithPrefixPath:(FPath *)path andSnapshotHolder:(FSnapshotHolder *)snapshotHolder;
+
+@property (strong, nonatomic) FSnapshotHolder* data;
+@property (strong, nonatomic) FPath* prefixPath;
+
+@end
+
+@implementation FIRMutableData
+
+@synthesize data;
+@synthesize prefixPath;
+
+- (id) initWithNode:(id<FNode>)node {
+ FSnapshotHolder* holder = [[FSnapshotHolder alloc] init];
+ FPath* path = [FPath empty];
+ [holder updateSnapshot:path withNewSnapshot:node];
+ return [self initWithPrefixPath:path andSnapshotHolder:holder];
+}
+
+- (id) initWithPrefixPath:(FPath *)path andSnapshotHolder:(FSnapshotHolder *)snapshotHolder {
+ self = [super init];
+ if (self) {
+ self.prefixPath = path;
+ self.data = snapshotHolder;
+ }
+ return self;
+}
+
+- (FIRMutableData *)childDataByAppendingPath:(NSString *)path {
+ FPath* wholePath = [self.prefixPath childFromString:path];
+ return [[FIRMutableData alloc] initWithPrefixPath:wholePath andSnapshotHolder:self.data];
+}
+
+- (FIRMutableData *) parent {
+ if ([self.prefixPath isEmpty]) {
+ return nil;
+ } else {
+ FPath* path = [self.prefixPath parent];
+ return [[FIRMutableData alloc] initWithPrefixPath:path andSnapshotHolder:self.data];
+ }
+}
+
+- (void) setValue:(id)aValue {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:aValue withValidationFrom:@"setValue:"];
+ [self.data updateSnapshot:self.prefixPath withNewSnapshot:node];
+}
+
+- (void) setPriority:(id)aPriority {
+ id<FNode> node = [self.data getNode:self.prefixPath];
+ id<FNode> pri = [FSnapshotUtilities nodeFrom:aPriority];
+ node = [node updatePriority:pri];
+ [self.data updateSnapshot:self.prefixPath withNewSnapshot:node];
+}
+
+- (id) value {
+ return [[self.data getNode:self.prefixPath] val];
+}
+
+- (id) priority {
+ return [[[self.data getNode:self.prefixPath] getPriority] val];
+}
+
+- (BOOL) hasChildren {
+ id<FNode> node = [self.data getNode:self.prefixPath];
+ return ![node isLeafNode] && ![(FChildrenNode*)node isEmpty];
+}
+
+- (BOOL) hasChildAtPath:(NSString *)path {
+ id<FNode> node = [self.data getNode:self.prefixPath];
+ FPath* childPath = [[FPath alloc] initWith:path];
+ return ![[node getChild:childPath] isEmpty];
+}
+
+- (NSUInteger) childrenCount {
+ return [[self.data getNode:self.prefixPath] numChildren];
+}
+
+- (NSString *) key {
+ return [self.prefixPath getBack];
+}
+
+- (id<FNode>) nodeValue {
+ return [self.data getNode:self.prefixPath];
+}
+
+- (NSEnumerator *) children {
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:self.nodeValue];
+ return [[FTransformedEnumerator alloc] initWithEnumerator:[indexedNode childEnumerator] andTransform:^id(FNamedNode *node) {
+ FPath* childPath = [self.prefixPath childFromString:node.name];
+ FIRMutableData * childData = [[FIRMutableData alloc] initWithPrefixPath:childPath andSnapshotHolder:self.data];
+ return childData;
+ }];
+}
+
+- (BOOL) isEqualToData:(FIRMutableData *)other {
+ return self.data == other.data && [[self.prefixPath description] isEqualToString:[other.prefixPath description]];
+}
+
+- (NSString *) description {
+ if (self.key == nil) {
+ return [NSString stringWithFormat:@"FIRMutableData (top-most transaction) %@ %@", self.key, self.value];
+ } else {
+ return [NSString stringWithFormat:@"FIRMutableData (%@) %@", self.key, self.value];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRServerValue.h b/Firebase/Database/Api/FIRServerValue.h
new file mode 100644
index 0000000..f5eadd5
--- /dev/null
+++ b/Firebase/Database/Api/FIRServerValue.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 "FIRDatabaseSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Placeholder values you may write into Firebase Database as a value or priority
+ * that will automatically be populated by the Firebase Database server.
+ */
+FIR_SWIFT_NAME(ServerValue)
+@interface FIRServerValue : NSObject
+
+/**
+ * Placeholder value for the number of milliseconds since the Unix epoch
+ */
++ (NSDictionary *) timestamp;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRServerValue.m b/Firebase/Database/Api/FIRServerValue.m
new file mode 100644
index 0000000..14bb745
--- /dev/null
+++ b/Firebase/Database/Api/FIRServerValue.m
@@ -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 "FIRDatabaseReference.h"
+#import "FIRServerValue.h"
+
+@implementation FIRServerValue
+
++ (NSDictionary *) timestamp {
+ static NSDictionary *timestamp = nil;
+ if (timestamp == nil) {
+ timestamp = @{ @".sv": @"timestamp" };
+ }
+ return timestamp;
+}
+
+@end
diff --git a/Firebase/Database/Api/FIRTransactionResult.h b/Firebase/Database/Api/FIRTransactionResult.h
new file mode 100644
index 0000000..3c2d39a
--- /dev/null
+++ b/Firebase/Database/Api/FIRTransactionResult.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "FIRDatabaseSwiftNameSupport.h"
+#import "FIRMutableData.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Used for runTransactionBlock:. An FIRTransactionResult instance is a container for the results of the transaction.
+ */
+FIR_SWIFT_NAME(TransactionResult)
+@interface FIRTransactionResult : NSObject
+
+/**
+ * Used for runTransactionBlock:. Indicates that the new value should be saved at this location
+ *
+ * @param value A FIRMutableData instance containing the new value to be set
+ * @return An FIRTransactionResult instance that can be used as a return value from the block given to runTransactionBlock:
+ */
++ (FIRTransactionResult *)successWithValue:(FIRMutableData *)value;
+
+
+/**
+ * Used for runTransactionBlock:. Indicates that the current transaction should no longer proceed.
+ *
+ * @return An FIRTransactionResult instance that can be used as a return value from the block given to runTransactionBlock:
+ */
++ (FIRTransactionResult *) abort;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/Api/FIRTransactionResult.m b/Firebase/Database/Api/FIRTransactionResult.m
new file mode 100644
index 0000000..8afc5b7
--- /dev/null
+++ b/Firebase/Database/Api/FIRTransactionResult.m
@@ -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 "FIRTransactionResult.h"
+#import "FIRTransactionResult_Private.h"
+
+@implementation FIRTransactionResult
+
+@synthesize update;
+@synthesize isSuccess;
+
++ (FIRTransactionResult *)successWithValue:(FIRMutableData *)value {
+ FIRTransactionResult * result = [[FIRTransactionResult alloc] init];
+ result.isSuccess = YES;
+ result.update = value;
+ return result;
+}
+
++ (FIRTransactionResult *) abort {
+ FIRTransactionResult * result = [[FIRTransactionResult alloc] init];
+ result.isSuccess = NO;
+ result.update = nil;
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/Api/FirebaseDatabase.h b/Firebase/Database/Api/FirebaseDatabase.h
new file mode 100644
index 0000000..e52f5d6
--- /dev/null
+++ b/Firebase/Database/Api/FirebaseDatabase.h
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+#ifndef FirebaseDatabase_h
+#define FirebaseDatabase_h
+
+#import "FIRDatabase.h"
+#import "FIRDatabaseQuery.h"
+#import "FIRDatabaseReference.h"
+#import "FIRDataEventType.h"
+#import "FIRDataSnapshot.h"
+#import "FIRMutableData.h"
+#import "FIRServerValue.h"
+#import "FIRTransactionResult.h"
+
+#endif /* FirebaseDatabase_h */
diff --git a/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h b/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h
new file mode 100644
index 0000000..4ff285b
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDataSnapshot_Private.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 "FIndexedNode.h"
+#import "FTypedefs_Private.h"
+
+@interface FIRDataSnapshot ()
+
+// in _Private for testing purposes
+@property (nonatomic, strong) FIndexedNode *node;
+
+- (id)initWithRef:(FIRDatabaseReference *)ref indexedNode:(FIndexedNode *)node;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h b/Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h
new file mode 100644
index 0000000..3a10fe3
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDatabaseQuery_Private.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "FPath.h"
+#import "FRepoManager.h"
+#import "FTypedefs_Private.h"
+#import "FQueryParams.h"
+#import "FIRDatabaseQuery.h"
+
+@interface FIRDatabaseQuery ()
+
++ (dispatch_queue_t)sharedQueue;
+
+- (id) initWithRepo:(FRepo *)repo path:(FPath *)path;
+- (id) initWithRepo:(FRepo *)repo
+ path:(FPath *)path
+ params:(FQueryParams *)params
+ orderByCalled:(BOOL)orderByCalled
+priorityMethodCalled:(BOOL)priorityMethodCalled;
+
+@property (nonatomic, strong) FRepo* repo;
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) FQueryParams *queryParams;
+@property (nonatomic) BOOL orderByCalled;
+@property (nonatomic) BOOL priorityMethodCalled;
+
+- (FQuerySpec *)querySpec;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRDatabaseReference_Private.h b/Firebase/Database/Api/Private/FIRDatabaseReference_Private.h
new file mode 100644
index 0000000..cb28feb
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDatabaseReference_Private.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "FIRDatabaseReference.h"
+#import "FTypedefs_Private.h"
+#import "FIRDatabaseConfig.h"
+#import "FRepo.h"
+
+@interface FIRDatabaseReference ()
+
+- (id)initWithConfig:(FIRDatabaseConfig *)config;
+- (id)initWithRepo:(FRepo *)repo path:(FPath *)path;
+
+// TODO: Update tests to not use this.
++ (FIRDatabaseConfig *)defaultConfig;
+@end
diff --git a/Firebase/Database/Api/Private/FIRDatabase_Private.h b/Firebase/Database/Api/Private/FIRDatabase_Private.h
new file mode 100644
index 0000000..5b7f8cc
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRDatabase_Private.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 "FIRDatabase.h"
+
+@class FRepo;
+@class FRepoInfo;
+@class FIRDatabaseConfig;
+
+@interface FIRDatabase ()
+
++ (NSString *) buildVersion;
++ (FIRDatabase *) createDatabaseForTests:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRMutableData_Private.h b/Firebase/Database/Api/Private/FIRMutableData_Private.h
new file mode 100644
index 0000000..ee3aa96
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRMutableData_Private.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 "FIRMutableData.h"
+#import "FNode.h"
+
+@interface FIRMutableData ()
+
+- (id) initWithNode:(id<FNode>)node;
+- (id<FNode>) nodeValue;
+- (BOOL) isEqualToData:(FIRMutableData *)other;
+
+@end
diff --git a/Firebase/Database/Api/Private/FIRTransactionResult_Private.h b/Firebase/Database/Api/Private/FIRTransactionResult_Private.h
new file mode 100644
index 0000000..82290f2
--- /dev/null
+++ b/Firebase/Database/Api/Private/FIRTransactionResult_Private.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 "FIRTransactionResult.h"
+#import "FIRMutableData.h"
+
+@interface FIRTransactionResult ()
+
+@property (nonatomic) BOOL isSuccess;
+@property (nonatomic, strong) FIRMutableData * update;
+
+@end
diff --git a/Firebase/Database/Api/Private/FTypedefs_Private.h b/Firebase/Database/Api/Private/FTypedefs_Private.h
new file mode 100644
index 0000000..73f4c9a
--- /dev/null
+++ b/Firebase/Database/Api/Private/FTypedefs_Private.h
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+#ifndef __FTYPEDEFS_PRIVATE__
+#define __FTYPEDEFS_PRIVATE__
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, FTransactionStatus) {
+ FTransactionInitializing, // 0
+ FTransactionRun, // 1
+ FTransactionSent, // 2
+ FTransactionCompleted, // 3
+ FTransactionSentNeedsAbort, // 4
+ FTransactionNeedsAbort // 5
+};
+
+@protocol FNode;
+@class FPath;
+@class FIRTransactionResult;
+@class FIRMutableData;
+@class FIRDataSnapshot;
+@class FCompoundHash;
+
+typedef void (^fbt_void_nserror_bool_datasnapshot) (NSError* error, BOOL committed, FIRDataSnapshot * snapshot);
+typedef FIRTransactionResult * (^fbt_transactionresult_mutabledata) (FIRMutableData * currentData);
+typedef void (^fbt_void_path_node) (FPath*, id<FNode>);
+typedef void (^fbt_void_nsstring) (NSString *);
+typedef BOOL (^fbt_bool_nsstring_node) (NSString *, id<FNode>);
+typedef void (^fbt_void_path_node_marray) (FPath *, id<FNode>, NSMutableArray *);
+typedef BOOL (^fbt_bool_void) (void);
+typedef void (^fbt_void_nsstring_nsstring)(NSString *str1, NSString* str2);
+typedef void (^fbt_void_nsstring_nserror)(NSString *str, NSError* error);
+typedef BOOL (^fbt_bool_path)(FPath *str);
+typedef void (^fbt_void_id)(id data);
+typedef NSString* (^fbt_nsstring_void) (void);
+typedef FCompoundHash* (^fbt_compoundhash_void) (void);
+typedef NSArray* (^fbt_nsarray_nsstring_id)(NSString *status, id Data);
+typedef NSArray* (^fbt_nsarray_nsstring)(NSString *status);
+
+// WWDC 2012 session 712 starting in page 83 for saving blocks in properties (use @property (strong) type name).
+
+#endif
diff --git a/Firebase/Database/Constants/FConstants.h b/Firebase/Database/Constants/FConstants.h
new file mode 100644
index 0000000..e97a8a1
--- /dev/null
+++ b/Firebase/Database/Constants/FConstants.h
@@ -0,0 +1,190 @@
+/*
+ * 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.
+ */
+
+#ifndef Firebase_FConstants_h
+#define Firebase_FConstants_h
+
+#import <Foundation/Foundation.h>
+
+#pragma mark -
+#pragma mark Wire Protocol Envelope Constants
+
+FOUNDATION_EXPORT NSString *const kFWPRequestType;
+FOUNDATION_EXPORT NSString *const kFWPRequestTypeData;
+FOUNDATION_EXPORT NSString *const kFWPRequestDataPayload;
+FOUNDATION_EXPORT NSString *const kFWPRequestNumber;
+FOUNDATION_EXPORT NSString *const kFWPRequestPayloadBody;
+FOUNDATION_EXPORT NSString *const kFWPRequestError;
+FOUNDATION_EXPORT NSString *const kFWPRequestAction;
+FOUNDATION_EXPORT NSString *const kFWPResponseForRNData;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionStatus;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionStatusOk;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionStatusDataStale;
+FOUNDATION_EXPORT NSString *const kFWPResponseForActionData;
+FOUNDATION_EXPORT NSString *const kFWPResponseDataWarnings;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerAction;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerPayloadBody;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdate;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataMerge;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataRangeMerge;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerAuthRevoked;
+FOUNDATION_EXPORT NSString *const kFWPASyncServerListenCancelled;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerSecurityDebug;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateBodyPath; // {“a”: “d”, “b”: {“p”: “/”, “d”: “<data>”}}
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateBodyData;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateStartPath;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateEndPath;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateRangeMerge;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataUpdateBodyTag;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataQueries;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerEnvelopeType;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerEnvelopeData;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessage;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageType;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageData;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerDataMessage;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHello;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloTimestamp;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloVersion;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloConnectedHost;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerHelloSession;
+
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageShutdown;
+FOUNDATION_EXPORT NSString *const kFWPAsyncServerControlMessageReset;
+
+#pragma mark -
+#pragma mark Wire Protocol Payload Constants
+
+FOUNDATION_EXPORT NSString *const kFWPRequestActionPut;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionMerge;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionTaggedListen;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionTaggedUnlisten;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionListen; // {"t": "d", "d": {"r": 1, "a": "l", "b": { "p": "/" } } }
+FOUNDATION_EXPORT NSString *const kFWPRequestActionUnlisten;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionStats;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionDisconnectPut;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionDisconnectMerge;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionDisconnectCancel;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionAuth;
+FOUNDATION_EXPORT NSString *const kFWPRequestActionUnauth;
+FOUNDATION_EXPORT NSString *const kFWPRequestCredential;
+FOUNDATION_EXPORT NSString *const kFWPRequestPath;
+FOUNDATION_EXPORT NSString *const kFWPRequestCounters;
+FOUNDATION_EXPORT NSString *const kFWPRequestQueries;
+FOUNDATION_EXPORT NSString *const kFWPRequestTag;
+FOUNDATION_EXPORT NSString *const kFWPRequestData;
+FOUNDATION_EXPORT NSString *const kFWPRequestHash;
+FOUNDATION_EXPORT NSString *const kFWPRequestCompoundHash;
+FOUNDATION_EXPORT NSString *const kFWPRequestCompoundHashPaths;
+FOUNDATION_EXPORT NSString *const kFWPRequestCompoundHashHashes;
+FOUNDATION_EXPORT NSString *const kFWPRequestStatus;
+
+#pragma mark -
+#pragma mark Websock Transport Constants
+
+FOUNDATION_EXPORT NSString *const kWireProtocolVersionParam;
+FOUNDATION_EXPORT NSString *const kWebsocketProtocolVersion;
+FOUNDATION_EXPORT NSString *const kWebsocketServerKillPacket;
+FOUNDATION_EXPORT const int kWebsocketMaxFrameSize;
+FOUNDATION_EXPORT NSUInteger const kWebsocketKeepaliveInterval;
+FOUNDATION_EXPORT NSUInteger const kWebsocketConnectTimeout;
+
+FOUNDATION_EXPORT float const kPersistentConnReconnectMinDelay;
+FOUNDATION_EXPORT float const kPersistentConnReconnectMaxDelay;
+FOUNDATION_EXPORT float const kPersistentConnReconnectMultiplier;
+FOUNDATION_EXPORT float const kPersistentConnSuccessfulConnectionEstablishedDelay;
+
+#pragma mark -
+#pragma mark Query / QueryParams constants
+
+FOUNDATION_EXPORT NSString *const kQueryDefault;
+FOUNDATION_EXPORT NSString *const kQueryDefaultObject;
+FOUNDATION_EXPORT NSString *const kViewManagerDictConstView;
+FOUNDATION_EXPORT NSString *const kFQPIndexStartValue;
+FOUNDATION_EXPORT NSString *const kFQPIndexStartName;
+FOUNDATION_EXPORT NSString *const kFQPIndexEndValue;
+FOUNDATION_EXPORT NSString *const kFQPIndexEndName;
+FOUNDATION_EXPORT NSString *const kFQPLimit;
+FOUNDATION_EXPORT NSString *const kFQPViewFrom;
+FOUNDATION_EXPORT NSString *const kFQPViewFromLeft;
+FOUNDATION_EXPORT NSString *const kFQPViewFromRight;
+FOUNDATION_EXPORT NSString *const kFQPIndex;
+
+#pragma mark -
+#pragma mark Interrupt Reasons
+
+FOUNDATION_EXPORT NSString *const kFInterruptReasonServerKill;
+FOUNDATION_EXPORT NSString *const kFInterruptReasonWaitingForOpen;
+FOUNDATION_EXPORT NSString *const kFInterruptReasonRepoInterrupt;
+FOUNDATION_EXPORT NSString *const kFInterruptReasonAuthExpired;
+
+#pragma mark -
+#pragma mark Payload constants
+
+FOUNDATION_EXPORT NSString *const kPayloadPriority;
+FOUNDATION_EXPORT NSString *const kPayloadValue;
+FOUNDATION_EXPORT NSString *const kPayloadMetadataPrefix;
+
+#pragma mark -
+#pragma mark ServerValue constants
+
+FOUNDATION_EXPORT NSString *const kServerValueSubKey;
+FOUNDATION_EXPORT NSString *const kServerValuePriority;
+
+#pragma mark -
+#pragma mark .info/ constants
+
+FOUNDATION_EXPORT NSString *const kDotInfoPrefix;
+FOUNDATION_EXPORT NSString *const kDotInfoConnected;
+FOUNDATION_EXPORT NSString *const kDotInfoServerTimeOffset;
+
+#pragma mark -
+#pragma mark ObjectiveC to JavaScript type constants
+
+FOUNDATION_EXPORT NSString *const kJavaScriptObject;
+FOUNDATION_EXPORT NSString *const kJavaScriptString;
+FOUNDATION_EXPORT NSString *const kJavaScriptBoolean;
+FOUNDATION_EXPORT NSString *const kJavaScriptNumber;
+FOUNDATION_EXPORT NSString *const kJavaScriptNull;
+FOUNDATION_EXPORT NSString *const kJavaScriptTrue;
+FOUNDATION_EXPORT NSString *const kJavaScriptFalse;
+
+#pragma mark -
+#pragma mark Error handling constants
+
+FOUNDATION_EXPORT NSString *const kFErrorDomain;
+FOUNDATION_EXPORT NSUInteger const kFAuthError;
+FOUNDATION_EXPORT NSString *const kFErrorWriteCanceled;
+
+#pragma mark -
+#pragma mark Validation Constants
+
+FOUNDATION_EXPORT NSUInteger const kFirebaseMaxObjectDepth;
+FOUNDATION_EXPORT const unsigned int kFirebaseMaxLeafSize;
+
+#pragma mark -
+#pragma mark Transaction Constants
+
+FOUNDATION_EXPORT NSUInteger const kFTransactionMaxRetries;
+FOUNDATION_EXPORT NSString *const kFTransactionTooManyRetries;
+FOUNDATION_EXPORT NSString *const kFTransactionNoData;
+FOUNDATION_EXPORT NSString *const kFTransactionSet;
+FOUNDATION_EXPORT NSString *const kFTransactionDisconnect;
+
+#endif
diff --git a/Firebase/Database/Constants/FConstants.m b/Firebase/Database/Constants/FConstants.m
new file mode 100644
index 0000000..e492ba1
--- /dev/null
+++ b/Firebase/Database/Constants/FConstants.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 "FConstants.h"
+
+#pragma mark -
+#pragma mark Wire Protocol Envelope Constants
+
+NSString *const kFWPRequestType = @"t";
+NSString *const kFWPRequestTypeData = @"d";
+NSString *const kFWPRequestDataPayload = @"d";
+NSString *const kFWPRequestNumber = @"r";
+NSString *const kFWPRequestPayloadBody = @"b";
+NSString *const kFWPRequestError = @"error";
+NSString *const kFWPRequestAction = @"a";
+NSString *const kFWPResponseForRNData = @"b";
+NSString *const kFWPResponseForActionStatus = @"s";
+NSString *const kFWPResponseForActionStatusOk = @"ok";
+NSString *const kFWPResponseForActionStatusDataStale = @"datastale";
+NSString *const kFWPResponseForActionData = @"d";
+NSString *const kFWPResponseDataWarnings = @"w";
+NSString *const kFWPAsyncServerAction = @"a";
+NSString *const kFWPAsyncServerPayloadBody = @"b";
+NSString *const kFWPAsyncServerDataUpdate = @"d";
+NSString *const kFWPAsyncServerDataMerge = @"m";
+NSString *const kFWPAsyncServerDataRangeMerge = @"rm";
+NSString *const kFWPAsyncServerAuthRevoked = @"ac";
+NSString *const kFWPASyncServerListenCancelled = @"c";
+NSString *const kFWPAsyncServerSecurityDebug = @"sd";
+NSString *const kFWPAsyncServerDataUpdateBodyPath = @"p"; // {“a”: “d”, “b”: {“p”: “/”, “d”: “<data>”}}
+NSString *const kFWPAsyncServerDataUpdateBodyData = @"d";
+NSString *const kFWPAsyncServerDataUpdateStartPath = @"s";
+NSString *const kFWPAsyncServerDataUpdateEndPath = @"e";
+NSString *const kFWPAsyncServerDataUpdateRangeMerge = @"m";
+NSString *const kFWPAsyncServerDataUpdateBodyTag = @"t";
+NSString *const kFWPAsyncServerDataQueries = @"q";
+
+NSString *const kFWPAsyncServerEnvelopeType = @"t";
+NSString *const kFWPAsyncServerEnvelopeData = @"d";
+NSString *const kFWPAsyncServerControlMessage = @"c";
+NSString *const kFWPAsyncServerControlMessageType = @"t";
+NSString *const kFWPAsyncServerControlMessageData = @"d";
+NSString *const kFWPAsyncServerDataMessage = @"d";
+
+NSString *const kFWPAsyncServerHello = @"h";
+NSString *const kFWPAsyncServerHelloTimestamp = @"ts";
+NSString *const kFWPAsyncServerHelloVersion = @"v";
+NSString *const kFWPAsyncServerHelloConnectedHost = @"h";
+NSString *const kFWPAsyncServerHelloSession = @"s";
+
+NSString *const kFWPAsyncServerControlMessageShutdown = @"s";
+NSString *const kFWPAsyncServerControlMessageReset = @"r";
+
+#pragma mark -
+#pragma mark Wire Protocol Payload Constants
+
+NSString *const kFWPRequestActionPut = @"p";
+NSString *const kFWPRequestActionMerge = @"m";
+NSString *const kFWPRequestActionListen = @"l"; // {"t": "d", "d": {"r": 1, "a": "l", "b": { "p": "/" } } }
+NSString *const kFWPRequestActionUnlisten = @"u";
+NSString *const kFWPRequestActionStats = @"s";
+NSString *const kFWPRequestActionTaggedListen = @"q";
+NSString *const kFWPRequestActionTaggedUnlisten = @"n";
+NSString *const kFWPRequestActionDisconnectPut = @"o";
+NSString *const kFWPRequestActionDisconnectMerge = @"om";
+NSString *const kFWPRequestActionDisconnectCancel = @"oc";
+NSString *const kFWPRequestActionAuth = @"auth";
+NSString *const kFWPRequestActionUnauth = @"unauth";
+NSString *const kFWPRequestCredential = @"cred";
+NSString *const kFWPRequestPath = @"p";
+NSString *const kFWPRequestCounters = @"c";
+NSString *const kFWPRequestQueries = @"q";
+NSString *const kFWPRequestTag = @"t";
+NSString *const kFWPRequestData = @"d";
+NSString *const kFWPRequestHash = @"h";
+NSString *const kFWPRequestCompoundHash = @"ch";
+NSString *const kFWPRequestCompoundHashPaths = @"ps";
+NSString *const kFWPRequestCompoundHashHashes = @"hs";
+NSString *const kFWPRequestStatus = @"s";
+
+#pragma mark -
+#pragma mark Websock Transport Constants
+
+NSString *const kWireProtocolVersionParam = @"v";
+NSString *const kWebsocketProtocolVersion = @"5";
+NSString *const kWebsocketServerKillPacket = @"kill";
+const int kWebsocketMaxFrameSize = 16384;
+NSUInteger const kWebsocketKeepaliveInterval = 45;
+NSUInteger const kWebsocketConnectTimeout = 30;
+
+float const kPersistentConnReconnectMinDelay = 1.0;
+float const kPersistentConnReconnectMaxDelay = 30.0;
+float const kPersistentConnReconnectMultiplier = 1.3f;
+float const kPersistentConnSuccessfulConnectionEstablishedDelay = 30.0;
+
+#pragma mark -
+#pragma mark Query constants
+
+NSString *const kQueryDefault = @"default";
+NSString *const kQueryDefaultObject = @"{}";
+NSString *const kViewManagerDictConstView = @"view";
+NSString *const kFQPIndexStartValue = @"sp";
+NSString *const kFQPIndexStartName = @"sn";
+NSString *const kFQPIndexEndValue = @"ep";
+NSString *const kFQPIndexEndName = @"en";
+NSString *const kFQPLimit = @"l";
+NSString *const kFQPViewFrom = @"vf";
+NSString *const kFQPViewFromLeft = @"l";
+NSString *const kFQPViewFromRight = @"r";
+NSString *const kFQPIndex = @"i";
+
+#pragma mark -
+#pragma mark Interrupt Reasons
+
+NSString *const kFInterruptReasonServerKill = @"server_kill";
+NSString *const kFInterruptReasonWaitingForOpen = @"waiting_for_open";
+NSString *const kFInterruptReasonRepoInterrupt = @"repo_interrupt";
+
+#pragma mark -
+#pragma mark Payload constants
+
+NSString *const kPayloadPriority = @".priority";
+NSString *const kPayloadValue = @".value";
+NSString *const kPayloadMetadataPrefix = @".";
+
+#pragma mark -
+#pragma mark ServerValue constants
+
+NSString *const kServerValueSubKey = @".sv";
+NSString *const kServerValuePriority = @"timestamp";
+
+#pragma mark -
+#pragma mark .info/ constants
+
+NSString *const kDotInfoPrefix = @".info";
+NSString *const kDotInfoConnected = @"connected";
+NSString *const kDotInfoServerTimeOffset = @"serverTimeOffset";
+
+#pragma mark -
+#pragma mark ObjectiveC to JavaScript type constants
+
+NSString *const kJavaScriptObject = @"object";
+NSString *const kJavaScriptString = @"string";
+NSString *const kJavaScriptBoolean = @"boolean";
+NSString *const kJavaScriptNumber = @"number";
+NSString *const kJavaScriptNull = @"null";
+NSString *const kJavaScriptTrue = @"true";
+NSString *const kJavaScriptFalse = @"false";
+
+#pragma mark -
+#pragma mark Error handling constants
+
+NSString *const kFErrorDomain = @"com.firebase";
+NSUInteger const kFAuthError = 1;
+NSString *const kFErrorWriteCanceled = @"write_canceled";
+
+#pragma mark -
+#pragma mark Validation Constants
+
+NSUInteger const kFirebaseMaxObjectDepth = 1000;
+const unsigned int kFirebaseMaxLeafSize = 1024 * 1024 * 10; // 10 MB
+
+#pragma mark -
+#pragma mark Transaction Constants
+
+NSUInteger const kFTransactionMaxRetries = 25;
+NSString *const kFTransactionTooManyRetries = @"maxretry";
+NSString *const kFTransactionNoData = @"nodata";
+NSString *const kFTransactionSet = @"set";
+NSString *const kFTransactionDisconnect = @"disconnect";
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
diff --git a/Firebase/Database/FClock.h b/Firebase/Database/FClock.h
new file mode 100644
index 0000000..1924ad4
--- /dev/null
+++ b/Firebase/Database/FClock.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 FClock <NSObject>
+
+- (NSTimeInterval)currentTime;
+
+@end
+
+@interface FSystemClock : NSObject<FClock>
+
++ (FSystemClock *)clock;
+
+@end
+
+@interface FOffsetClock : NSObject<FClock>
+
+- (id)initWithClock:(id<FClock>)clock offset:(NSTimeInterval)offset;
+
+@end
diff --git a/Firebase/Database/FClock.m b/Firebase/Database/FClock.m
new file mode 100644
index 0000000..2464056
--- /dev/null
+++ b/Firebase/Database/FClock.m
@@ -0,0 +1,58 @@
+/*
+ * 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 "FClock.h"
+
+@implementation FSystemClock
+
+- (NSTimeInterval)currentTime {
+ return [[NSDate date] timeIntervalSince1970];
+}
+
++ (FSystemClock *)clock {
+ static dispatch_once_t onceToken;
+ static FSystemClock *clock;
+ dispatch_once(&onceToken, ^{
+ clock = [[FSystemClock alloc] init];
+ });
+ return clock;
+}
+
+@end
+
+@interface FOffsetClock ()
+
+@property (nonatomic, strong) id<FClock> clock;
+@property (nonatomic) NSTimeInterval offset;
+
+@end
+
+@implementation FOffsetClock
+
+- (NSTimeInterval)currentTime {
+ return [self.clock currentTime] + self.offset;
+}
+
+- (id)initWithClock:(id<FClock>)clock offset:(NSTimeInterval)offset {
+ self = [super init];
+ if (self != nil) {
+ self->_clock = clock;
+ self->_offset = offset;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/FEventGenerator.h b/Firebase/Database/FEventGenerator.h
new file mode 100644
index 0000000..1bc011b
--- /dev/null
+++ b/Firebase/Database/FEventGenerator.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>
+
+@class FQuerySpec;
+@class FIndexedNode;
+@protocol FNode;
+
+@interface FEventGenerator : NSObject
+- (id) initWithQuery:(FQuerySpec *)query;
+- (NSArray*) generateEventsForChanges:(NSArray*)changes eventCache:(FIndexedNode *)eventCache
+ eventRegistrations:(NSArray*)registrations;
+@end
diff --git a/Firebase/Database/FEventGenerator.m b/Firebase/Database/FEventGenerator.m
new file mode 100644
index 0000000..f6e8f47
--- /dev/null
+++ b/Firebase/Database/FEventGenerator.m
@@ -0,0 +1,141 @@
+/*
+ * 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 "FEventGenerator.h"
+#import "FNode.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQueryParams.h"
+#import "FQuerySpec.h"
+#import "FChange.h"
+#import "FNamedNode.h"
+#import "FEventRegistration.h"
+#import "FEvent.h"
+#import "FDataEvent.h"
+
+@interface FEventGenerator ()
+@property (nonatomic, strong) FQuerySpec *query;
+@end
+
+/**
+* An EventGenerator is used to convert "raw" changes (fb.core.view.Change) as computed by the
+* CacheDiffer into actual events (fb.core.view.Event) that can be raised. See generateEventsForChanges()
+* for details.
+*/
+@implementation FEventGenerator
+
+- (id)initWithQuery:(FQuerySpec *)query {
+ self = [super init];
+ if (self) {
+ self.query = query;
+ }
+ return self;
+}
+
+/**
+* Given a set of raw changes (no moved events, and prevName not specified yet), and a set of EventRegistrations that
+* should be notified of these changes, generate the actual events to be raised.
+*
+* Notes:
+* - child_moved events will be synthesized at this time for any child_changed events that affect our index
+* - prevName will be calculated based on the index ordering
+*
+* @param changes NSArray of FChange, not necessarily in order.
+* @param registrations is NSArray of FEventRegistration.
+* @return NSArray of FEvent.
+*/
+- (NSArray *) generateEventsForChanges:(NSArray *)changes
+ eventCache:(FIndexedNode *)eventCache
+ eventRegistrations:(NSArray *)registrations
+{
+ NSMutableArray *events = [[NSMutableArray alloc] init];
+
+ // child_moved is index-specific, so check all our child_changed events to see if we need to materialize
+ // child_moved events with this view's index
+ NSMutableArray *moves = [[NSMutableArray alloc] init];
+ for (FChange *change in changes) {
+ if (change.type == FIRDataEventTypeChildChanged && [self.query.index indexedValueChangedBetween:change.oldIndexedNode.node
+ and:change.indexedNode.node]) {
+ FChange *moveChange = [[FChange alloc] initWithType:FIRDataEventTypeChildMoved
+ indexedNode:change.indexedNode
+ childKey:change.childKey
+ oldIndexedNode:nil];
+ [moves addObject:moveChange];
+ }
+ }
+
+ [self generateEvents:events forType:FIRDataEventTypeChildRemoved changes:changes eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeChildAdded changes:changes eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeChildMoved changes:moves eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeChildChanged changes:changes eventCache:eventCache eventRegistrations:registrations];
+ [self generateEvents:events forType:FIRDataEventTypeValue changes:changes eventCache:eventCache eventRegistrations:registrations];
+
+ return events;
+}
+
+- (void) generateEvents:(NSMutableArray *)events
+ forType:(FIRDataEventType)eventType
+ changes:(NSArray *)changes
+ eventCache:(FIndexedNode *)eventCache
+ eventRegistrations:(NSArray *)registrations
+{
+ NSMutableArray *filteredChanges = [[NSMutableArray alloc] init];
+ for (FChange *change in changes) {
+ if (change.type == eventType) {
+ [filteredChanges addObject:change];
+ }
+ }
+
+ id<FIndex> index = self.query.index;
+
+ [filteredChanges sortUsingComparator:^NSComparisonResult(FChange *one, FChange *two) {
+ if (one.childKey == nil || two.childKey == nil) {
+ @throw [[NSException alloc] initWithName:@"InternalInconsistencyError"
+ reason:@"Should only compare child_ events"
+ userInfo:nil];
+ }
+ return [index compareKey:one.childKey
+ andNode:one.indexedNode.node
+ toOtherKey:two.childKey
+ andNode:two.indexedNode.node];
+ }];
+
+ for (FChange *change in filteredChanges) {
+ for (id<FEventRegistration> registration in registrations) {
+ if ([registration responseTo:eventType]) {
+ id<FEvent> event = [self generateEventForChange:change registration:registration eventCache:eventCache];
+ [events addObject:event];
+ }
+ }
+ }
+}
+
+- (id<FEvent>) generateEventForChange:(FChange *)change
+ registration:(id<FEventRegistration>)registration
+ eventCache:(FIndexedNode *)eventCache
+{
+ FChange *materializedChange;
+ if (change.type == FIRDataEventTypeValue || change.type == FIRDataEventTypeChildRemoved) {
+ materializedChange = change;
+ } else {
+ NSString *prevChildKey = [eventCache predecessorForChildKey:change.childKey
+ childNode:change.indexedNode.node
+ index:self.query.index];
+ materializedChange = [change changeWithPrevKey:prevChildKey];
+ }
+ return [registration createEventFrom:materializedChange query:self.query];
+}
+
+@end
diff --git a/Firebase/Database/FIRDatabaseConfig_Private.h b/Firebase/Database/FIRDatabaseConfig_Private.h
new file mode 100644
index 0000000..b0a9dc4
--- /dev/null
+++ b/Firebase/Database/FIRDatabaseConfig_Private.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 "FIRDatabaseConfig.h"
+#import "FAuthTokenProvider.h"
+
+@protocol FStorageEngine;
+
+@interface FIRDatabaseConfig ()
+
+@property (nonatomic, readonly) BOOL isFrozen;
+@property (nonatomic, strong, readonly) NSString *sessionIdentifier;
+@property (nonatomic, strong) id<FAuthTokenProvider> authTokenProvider;
+@property (nonatomic, strong) id<FStorageEngine> forceStorageEngine;
+
+- (void)freeze;
+
++ (FIRDatabaseConfig *)configForName:(NSString *)name;
+
++ (FIRDatabaseConfig *)defaultConfig;
+
+@end
diff --git a/Firebase/Database/FIRDatabaseReference.h b/Firebase/Database/FIRDatabaseReference.h
new file mode 100644
index 0000000..eb3fecd
--- /dev/null
+++ b/Firebase/Database/FIRDatabaseReference.h
@@ -0,0 +1,719 @@
+/*
+ * 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 "FIRDatabaseQuery.h"
+#import "FIRDatabase.h"
+#import "FIRDatabaseSwiftNameSupport.h"
+#import "FIRDataSnapshot.h"
+#import "FIRMutableData.h"
+#import "FIRTransactionResult.h"
+#import "FIRServerValue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDatabase;
+
+/**
+ * A FIRDatabaseReference represents a particular location in your Firebase Database
+ * and can be used for reading or writing data to that Firebase Database location.
+ *
+ * This class is the starting point for all Firebase Database operations. After you've
+ * obtained your first FIRDatabaseReference via [FIRDatabase reference], you can use it
+ * to read data (ie. observeEventType:withBlock:), write data (ie. setValue:), and to
+ * create new FIRDatabaseReferences (ie. child:).
+ */
+FIR_SWIFT_NAME(DatabaseReference)
+@interface FIRDatabaseReference : FIRDatabaseQuery
+
+
+#pragma mark - Getting references to children locations
+
+/**
+ * Gets a FIRDatabaseReference for the location at the specified relative path.
+ * The relative path can either be a simple child key (e.g. 'fred') or a
+ * deeper slash-separated path (e.g. 'fred/name/first').
+ *
+ * @param pathString A relative path from this location to the desired child location.
+ * @return A FIRDatabaseReference for the specified relative path.
+ */
+- (FIRDatabaseReference *)child:(NSString *)pathString;
+
+/**
+ * childByAppendingPath: is deprecated, use child: instead.
+ */
+- (FIRDatabaseReference *)childByAppendingPath:(NSString *)pathString __deprecated_msg("use child: instead");
+
+/**
+ * childByAutoId generates a new child location using a unique key and returns a
+ * FIRDatabaseReference to it. This is useful when the children of a Firebase Database
+ * location represent a list of items.
+ *
+ * The unique key generated by childByAutoId: is prefixed with a client-generated
+ * timestamp so that the resulting list will be chronologically-sorted.
+ *
+ * @return A FIRDatabaseReference for the generated location.
+ */
+- (FIRDatabaseReference *) childByAutoId;
+
+
+#pragma mark - Writing data
+
+/** Write data to this Firebase Database location.
+
+This will overwrite any data at this location and all child locations.
+
+Data types that can be set are:
+
+- NSString -- @"Hello World"
+- NSNumber (also includes boolean) -- @YES, @43, @4.333
+- NSDictionary -- @{@"key": @"value", @"nested": @{@"another": @"value"} }
+- NSArray
+
+The effect of the write will be visible immediately and the corresponding
+events will be triggered. Synchronization of the data to the Firebase Database
+servers will also be started.
+
+Passing null for the new value is equivalent to calling remove:;
+all data at this location or any child location will be deleted.
+
+Note that setValue: will remove any priority stored at this location, so if priority
+is meant to be preserved, you should use setValue:andPriority: instead.
+
+@param value The value to be written.
+ */
+- (void) setValue:(nullable id)value;
+
+
+/**
+ * The same as setValue: with a block that gets triggered after the write operation has
+ * been committed to the Firebase Database servers.
+ *
+ * @param value The value to be written.
+ * @param block The block to be called after the write has been committed to the Firebase Database servers.
+ */
+- (void) setValue:(nullable id)value withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * The same as setValue: with an additional priority to be attached to the data being written.
+ * Priorities are used to order items.
+ *
+ * @param value The value to be written.
+ * @param priority The priority to be attached to that data.
+ */
+- (void) setValue:(nullable id)value andPriority:(nullable id)priority;
+
+
+/**
+ * The same as setValue:andPriority: with a block that gets triggered after the write operation has
+ * been committed to the Firebase Database servers.
+ *
+ * @param value The value to be written.
+ * @param priority The priority to be attached to that data.
+ * @param block The block to be called after the write has been committed to the Firebase Database servers.
+ */
+- (void) setValue:(nullable id)value andPriority:(nullable id)priority withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Remove the data at this Firebase Database location. Any data at child locations will also be deleted.
+ *
+ * The effect of the delete will be visible immediately and the corresponding events
+ * will be triggered. Synchronization of the delete to the Firebase Database servers will
+ * also be started.
+ *
+ * remove: is equivalent to calling setValue:nil
+ */
+- (void) removeValue;
+
+
+/**
+ * The same as remove: with a block that gets triggered after the remove operation has
+ * been committed to the Firebase Database servers.
+ *
+ * @param block The block to be called after the remove has been committed to the Firebase Database servers.
+ */
+- (void) removeValueWithCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+/**
+ * Sets a priority for the data at this Firebase Database location.
+ * Priorities can be used to provide a custom ordering for the children at a location
+ * (if no priorities are specified, the children are ordered by key).
+ *
+ * You cannot set a priority on an empty location. For this reason
+ * setValue:andPriority: should be used when setting initial data with a specific priority
+ * and setPriority: should be used when updating the priority of existing data.
+ *
+ * Children are sorted based on this priority using the following rules:
+ *
+ * Children with no priority come first.
+ * Children with a number as their priority come next. They are sorted numerically by priority (small to large).
+ * Children with a string as their priority come last. They are sorted lexicographically by priority.
+ * Whenever two children have the same priority (including no priority), they are sorted by key. Numeric
+ * keys come first (sorted numerically), followed by the remaining keys (sorted lexicographically).
+ *
+ * Note that priorities are parsed and ordered as IEEE 754 double-precision floating-point numbers.
+ * Keys are always stored as strings and are treated as numbers only when they can be parsed as a
+ * 32-bit integer
+ *
+ * @param priority The priority to set at the specified location.
+ */
+- (void) setPriority:(nullable id)priority;
+
+
+/**
+ * The same as setPriority: with a block that is called once the priority has
+ * been committed to the Firebase Database servers.
+ *
+ * @param priority The priority to set at the specified location.
+ * @param block The block that is triggered after the priority has been written on the servers.
+ */
+- (void) setPriority:(nullable id)priority withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+/**
+ * Updates the values at the specified paths in the dictionary without overwriting other
+ * keys at this location.
+ *
+ * @param values A dictionary of the keys to change and their new values
+ */
+- (void) updateChildValues:(NSDictionary *)values;
+
+/**
+ * The same as update: with a block that is called once the update has been committed to the
+ * Firebase Database servers
+ *
+ * @param values A dictionary of the keys to change and their new values
+ * @param block The block that is triggered after the update has been written on the Firebase Database servers
+ */
+- (void) updateChildValues:(NSDictionary *)values withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+#pragma mark - Attaching observers to read data
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * observeEventType:withBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * observeEventType:andPreviousSiblingKeyWithBlock: is used to listen for data changes at a particular location.
+ * This is the primary way to read data from the Firebase Database. Your block will be triggered
+ * for the initial data and again whenever the data changes. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you will no longer receive new events due to no longer having permission.
+ *
+ * Use removeObserverWithHandle: to stop receiving updates.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called with initial data and updates. It is passed the data as a FIRDataSnapshot
+ * and the previous child's key.
+ * @param cancelBlock The block that should be called if this client no longer has permission to receive these events
+ * @return A handle used to unregister this block later using removeObserverWithHandle:
+ */
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *snapshot))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+
+/**
+ * This is equivalent to observeEventType:withBlock:, except the block is immediately canceled after the initial data is returned. In addition, for FIRDataEventTypeChildAdded, FIRDataEventTypeChildMoved, and
+ * FIRDataEventTypeChildChanged events, your block will be passed the key of the previous node by priority order.
+ *
+ * The cancelBlock will be called if you do not have permission to read data at this location.
+ *
+ * @param eventType The type of event to listen for.
+ * @param block The block that should be called. It is passed the data as a FIRDataSnapshot and the previous child's key.
+ * @param cancelBlock The block that will be called if you don't have permission to access this data
+ */
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(void (^)(FIRDataSnapshot *snapshot, NSString *__nullable prevKey))block withCancelBlock:(nullable void (^)(NSError* error))cancelBlock;
+
+#pragma mark - Detaching observers
+
+/**
+ * Detach a block previously attached with observeEventType:withBlock:.
+ *
+ * @param handle The handle returned by the call to observeEventType:withBlock: which we are trying to remove.
+ */
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle;
+
+/**
+ * By calling `keepSynced:YES` on a location, the data for that location will automatically be downloaded and
+ * kept in sync, even when no listeners are attached for that location. Additionally, while a location is kept
+ * synced, it will not be evicted from the persistent disk cache.
+ *
+ * @param keepSynced Pass YES to keep this location synchronized, pass NO to stop synchronization.
+ */
+- (void) keepSynced:(BOOL)keepSynced;
+
+
+/**
+ * Removes all observers at the current reference, but does not remove any observers at child references.
+ * removeAllObservers must be called again for each child reference where a listener was established to remove the observers.
+ */
+- (void) removeAllObservers;
+
+#pragma mark - Querying and limiting
+
+
+/**
+ * queryLimitedToFirst: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryLimitedToFirst: will respond to at most the first limit child nodes.
+ *
+ * @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+ * @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+ */
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit;
+
+
+/**
+ * queryLimitedToLast: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryLimitedToLast: will respond to at most the last limit child nodes.
+ *
+ * @param limit The upper bound, inclusive, for the number of child nodes to receive events for
+ * @return A FIRDatabaseQuery instance, limited to at most limit child nodes.
+ */
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit;
+
+/**
+ * queryOrderBy: is used to generate a reference to a view of the data that's been sorted by the values of
+ * a particular child key. This method is intended to be used in combination with queryStartingAtValue:,
+ * queryEndingAtValue:, or queryEqualToValue:.
+ *
+ * @param key The child key to use in ordering data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, ordered by the values of the specified child key.
+ */
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)key;
+
+/**
+ * queryOrderedByKey: is used to generate a reference to a view of the data that's been sorted by child key.
+ * This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child keys.
+ */
+- (FIRDatabaseQuery *) queryOrderedByKey;
+
+/**
+ * queryOrderedByPriority: is used to generate a reference to a view of the data that's been sorted by child
+ * priority. This method is intended to be used in combination with queryStartingAtValue:, queryEndingAtValue:,
+ * or queryEqualToValue:.
+ *
+ * @return A FIRDatabaseQuery instance, ordered by child priorities.
+ */
+- (FIRDatabaseQuery *) queryOrderedByPriority;
+
+/**
+ * queryStartingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue: will respond to events at nodes with a value
+ * greater than or equal to startValue.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue;
+
+/**
+ * queryStartingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryStartingAtValue:childKey will respond to events at nodes with a value
+ * greater than startValue, or equal to startValue and with a key greater than or equal to childKey.
+ *
+ * @param startValue The lower bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The lower bound, inclusive, for the key of nodes with value equal to startValue
+ * @return A FIRDatabaseQuery instance, limited to data with value greater than or equal to startValue
+ */
+- (FIRDatabaseQuery *)queryStartingAtValue:(nullable id)startValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEndingAtValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue: will respond to events at nodes with a value
+ * less than or equal to endValue.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue;
+
+/**
+ * queryEndingAtValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEndingAtValue:childKey will respond to events at nodes with a value
+ * less than endValue, or equal to endValue and with a key less than or equal to childKey.
+ *
+ * @param endValue The upper bound, inclusive, for the value of data visible to the returned FIRDatabaseQuery
+ * @param childKey The upper bound, inclusive, for the key of nodes with value equal to endValue
+ * @return A FIRDatabaseQuery instance, limited to data with value less than or equal to endValue
+ */
+- (FIRDatabaseQuery *)queryEndingAtValue:(nullable id)endValue childKey:(nullable NSString *)childKey;
+
+/**
+ * queryEqualToValue: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue: will respond to events at nodes with a value equal
+ * to the supplied argument.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value;
+
+/**
+ * queryEqualToValue:childKey: is used to generate a reference to a limited view of the data at this location.
+ * The FIRDatabaseQuery instance returned by queryEqualToValue:childKey will respond to events at nodes with a value
+ * equal to the supplied argument with a key equal to childKey. There will be at most one node that matches because
+ * child keys are unique.
+ *
+ * @param value The value that the data returned by this FIRDatabaseQuery will have
+ * @param childKey The key of nodes with the right value
+ * @return A FIRDatabaseQuery instance, limited to data with the supplied value and the key.
+ */
+- (FIRDatabaseQuery *)queryEqualToValue:(nullable id)value childKey:(nullable NSString *)childKey;
+
+#pragma mark - Managing presence
+
+/**
+ * Ensure the data at this location is set to the specified value when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * onDisconnectSetValue: is especially useful for implementing "presence" systems,
+ * where a value should be changed or cleared when a user disconnects
+ * so that he appears "offline" to other users.
+ *
+ * @param value The value to be set after the connection is lost.
+ */
+- (void) onDisconnectSetValue:(nullable id)value;
+
+
+/**
+ * Ensure the data at this location is set to the specified value when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * The completion block will be triggered when the operation has been successfully queued up on the Firebase Database servers
+ *
+ * @param value The value to be set after the connection is lost.
+ * @param block Block to be triggered when the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectSetValue:(nullable id)value withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Ensure the data at this location is set to the specified value and priority when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * @param value The value to be set after the connection is lost.
+ * @param priority The priority to be set after the connection is lost.
+ */
+- (void) onDisconnectSetValue:(nullable id)value andPriority:(id)priority;
+
+
+/**
+ * Ensure the data at this location is set to the specified value and priority when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ * The completion block will be triggered when the operation has been successfully queued up on the Firebase Database servers
+ *
+ * @param value The value to be set after the connection is lost.
+ * @param priority The priority to be set after the connection is lost.
+ * @param block Block to be triggered when the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectSetValue:(nullable id)value andPriority:(nullable id)priority withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Ensure the data at this location is removed when
+ * the client is disconnected (due to closing the app, navigating
+ * to a new page, or network issues).
+ *
+ * onDisconnectRemoveValue is especially useful for implementing "presence" systems.
+ */
+- (void) onDisconnectRemoveValue;
+
+
+/**
+ * Ensure the data at this location is removed when
+ * the client is disconnected (due to closing the app, navigating
+ * to a new page, or network issues).
+ *
+ * onDisconnectRemoveValueWithCompletionBlock: is especially useful for implementing "presence" systems.
+ *
+ * @param block Block to be triggered when the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectRemoveValueWithCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+
+/**
+ * Ensure the data has the specified child values updated when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ *
+ * @param values A dictionary of child node keys and the values to set them to after the connection is lost.
+ */
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values;
+
+
+/**
+ * Ensure the data has the specified child values updated when
+ * the client is disconnected (due to closing the browser, navigating
+ * to a new page, or network issues).
+ *
+ *
+ * @param values A dictionary of child node keys and the values to set them to after the connection is lost.
+ * @param block A block that will be called once the operation has been queued up on the Firebase Database servers
+ */
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values withCompletionBlock:(void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+/**
+ * Cancel any operations that are set to run on disconnect. If you previously called onDisconnectSetValue:,
+ * onDisconnectRemoveValue:, or onDisconnectUpdateChildValues:, and no longer want the values updated when the
+ * connection is lost, call cancelDisconnectOperations:
+ */
+- (void) cancelDisconnectOperations;
+
+
+/**
+ * Cancel any operations that are set to run on disconnect. If you previously called onDisconnectSetValue:,
+ * onDisconnectRemoveValue:, or onDisconnectUpdateChildValues:, and no longer want the values updated when the
+ * connection is lost, call cancelDisconnectOperations:
+ *
+ * @param block A block that will be triggered once the Firebase Database servers have acknowledged the cancel request.
+ */
+- (void) cancelDisconnectOperationsWithCompletionBlock:(nullable void (^)(NSError *__nullable error, FIRDatabaseReference * ref))block;
+
+
+#pragma mark - Manual Connection Management
+
+/**
+ * Manually disconnect the Firebase Database client from the server and disable automatic reconnection.
+ *
+ * The Firebase Database client automatically maintains a persistent connection to the Firebase Database server,
+ * which will remain active indefinitely and reconnect when disconnected. However, the goOffline( )
+ * and goOnline( ) methods may be used to manually control the client connection in cases where
+ * a persistent connection is undesirable.
+ *
+ * While offline, the Firebase Database client will no longer receive data updates from the server. However,
+ * all database operations performed locally will continue to immediately fire events, allowing
+ * your application to continue behaving normally. Additionally, each operation performed locally
+ * will automatically be queued and retried upon reconnection to the Firebase Database server.
+ *
+ * To reconnect to the Firebase Database server and begin receiving remote events, see goOnline( ).
+ * Once the connection is reestablished, the Firebase Database client will transmit the appropriate data
+ * and fire the appropriate events so that your client "catches up" automatically.
+ *
+ * Note: Invoking this method will impact all Firebase Database connections.
+ */
++ (void) goOffline;
+
+/**
+ * Manually reestablish a connection to the Firebase Database server and enable automatic reconnection.
+ *
+ * The Firebase Database client automatically maintains a persistent connection to the Firebase Database server,
+ * which will remain active indefinitely and reconnect when disconnected. However, the goOffline( )
+ * and goOnline( ) methods may be used to manually control the client connection in cases where
+ * a persistent connection is undesirable.
+ *
+ * This method should be used after invoking goOffline( ) to disable the active connection.
+ * Once reconnected, the Firebase Database client will automatically transmit the proper data and fire
+ * the appropriate events so that your client "catches up" automatically.
+ *
+ * To disconnect from the Firebase Database server, see goOffline( ).
+ *
+ * Note: Invoking this method will impact all Firebase Database connections.
+ */
++ (void) goOnline;
+
+
+#pragma mark - Transactions
+
+/**
+ * Performs an optimistic-concurrency transactional update to the data at this location. Your block will be called with a FIRMutableData
+ * instance that contains the current data at this location. Your block should update this data to the value you
+ * wish to write to this location, and then return an instance of FIRTransactionResult with the new data.
+ *
+ * If, when the operation reaches the server, it turns out that this client had stale data, your block will be run
+ * again with the latest data from the server.
+ *
+ * When your block is run, you may decide to abort the transaction by returning [FIRTransactionResult abort].
+ *
+ * @param block This block receives the current data at this location and must return an instance of FIRTransactionResult
+ */
+- (void) runTransactionBlock:(FIRTransactionResult * (^) (FIRMutableData* currentData))block;
+
+
+/**
+ * Performs an optimistic-concurrency transactional update to the data at this location. Your block will be called with a FIRMutableData
+ * instance that contains the current data at this location. Your block should update this data to the value you
+ * wish to write to this location, and then return an instance of FIRTransactionResult with the new data.
+ *
+ * If, when the operation reaches the server, it turns out that this client had stale data, your block will be run
+ * again with the latest data from the server.
+ *
+ * When your block is run, you may decide to abort the transaction by returning [FIRTransactionResult abort].
+ *
+ * @param block This block receives the current data at this location and must return an instance of FIRTransactionResult
+ * @param completionBlock This block will be triggered once the transaction is complete, whether it was successful or not. It will indicate if there was an error, whether or not the data was committed, and what the current value of the data at this location is.
+ */
+- (void)runTransactionBlock:(FIRTransactionResult * (^) (FIRMutableData* currentData))block andCompletionBlock:(void (^) (NSError *__nullable error, BOOL committed, FIRDataSnapshot *__nullable snapshot))completionBlock;
+
+
+
+/**
+ * Performs an optimistic-concurrency transactional update to the data at this location. Your block will be called with a FIRMutableData
+ * instance that contains the current data at this location. Your block should update this data to the value you
+ * wish to write to this location, and then return an instance of FIRTransactionResult with the new data.
+ *
+ * If, when the operation reaches the server, it turns out that this client had stale data, your block will be run
+ * again with the latest data from the server.
+ *
+ * When your block is run, you may decide to abort the transaction by return [FIRTransactionResult abort].
+ *
+ * Since your block may be run multiple times, this client could see several immediate states that don't exist on the server. You can suppress those immediate states until the server confirms the final state of the transaction.
+ *
+ * @param block This block receives the current data at this location and must return an instance of FIRTransactionResult
+ * @param completionBlock This block will be triggered once the transaction is complete, whether it was successful or not. It will indicate if there was an error, whether or not the data was committed, and what the current value of the data at this location is.
+ * @param localEvents Set this to NO to suppress events raised for intermediate states, and only get events based on the final state of the transaction.
+ */
+- (void)runTransactionBlock:(FIRTransactionResult * (^) (FIRMutableData* currentData))block andCompletionBlock:(nullable void (^) (NSError *__nullable error, BOOL committed, FIRDataSnapshot *__nullable snapshot))completionBlock withLocalEvents:(BOOL)localEvents;
+
+
+#pragma mark - Retrieving String Representation
+
+/**
+ * Gets the absolute URL of this Firebase Database location.
+ *
+ * @return The absolute URL of the referenced Firebase Database location.
+ */
+- (NSString *) description;
+
+#pragma mark - Properties
+
+/**
+ * Gets a FIRDatabaseReference for the parent location.
+ * If this instance refers to the root of your Firebase Database, it has no parent,
+ * and therefore parent( ) will return null.
+ *
+ * @return A FIRDatabaseReference for the parent location.
+ */
+@property (strong, readonly, nonatomic, nullable) FIRDatabaseReference * parent;
+
+
+/**
+ * Gets a FIRDatabaseReference for the root location
+ *
+ * @return A new FIRDatabaseReference to root location.
+ */
+@property (strong, readonly, nonatomic) FIRDatabaseReference * root;
+
+
+/**
+ * Gets the last token in a Firebase Database location (e.g. 'fred' in https&#58;//SampleChat.firebaseIO-demo.com/users/fred)
+ *
+ * @return The key of the location this reference points to.
+ */
+@property (strong, readonly, nonatomic) NSString* key;
+
+/**
+ * Gets the URL for the Firebase Database location referenced by this FIRDatabaseReference.
+ *
+ * @return The url of the location this reference points to.
+ */
+@property (strong, readonly, nonatomic) NSString* URL;
+
+/**
+ * Gets the FIRDatabase instance associated with this reference.
+ *
+ * @return The FIRDatabase object for this reference.
+ */
+@property (strong, readonly, nonatomic) FIRDatabase *database;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firebase/Database/FIRDatabaseReference.m b/Firebase/Database/FIRDatabaseReference.m
new file mode 100644
index 0000000..4f27493
--- /dev/null
+++ b/Firebase/Database/FIRDatabaseReference.m
@@ -0,0 +1,404 @@
+/*
+ * 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 "FIRApp.h"
+#import "FIRDatabaseReference.h"
+#import "FIROptions.h"
+#import "FUtilities.h"
+#import "FNextPushId.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FValidation.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FStringUtilities.h"
+#import "FSnapshotUtilities.h"
+#import "FIRDatabaseConfig.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FQueryParams.h"
+#import "FIRDatabase.h"
+
+@implementation FIRDatabaseReference
+
++ (FIRDatabaseConfig *)defaultConfig {
+ return [FIRDatabaseConfig defaultConfig];
+}
+
+#pragma mark -
+#pragma mark Constructors
+
+- (id) initWithConfig:(FIRDatabaseConfig *)config {
+ FParsedUrl* parsedUrl = [FUtilities parseUrl:[[FIRApp defaultApp] options].databaseURL];
+ [FValidation validateFrom:@"initWithUrl:" validURL:parsedUrl];
+ return [self initWithRepo:[FRepoManager getRepo:parsedUrl.repoInfo config:config] path:parsedUrl.path];
+}
+
+- (id) initWithRepo:(FRepo *)repo path:(FPath *)path {
+ return [super initWithRepo:repo
+ path:path
+ params:[FQueryParams defaultInstance]
+ orderByCalled:NO
+ priorityMethodCalled:NO];
+}
+
+
+#pragma mark -
+#pragma mark Ancillary methods
+
+- (NSString *) key {
+ if([self.path isEmpty]) {
+ return nil;
+ }
+ else {
+ return [self.path getBack];
+ }
+}
+
+- (FIRDatabase *) database {
+ return self.repo.database;
+}
+
+- (FIRDatabaseReference *) parent {
+ FPath* parentPath = [self.path parent];
+ FIRDatabaseReference * parent = nil;
+ if (parentPath != nil ) {
+ parent = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:parentPath];
+ }
+ return parent;
+}
+
+- (NSString *) URL {
+ FIRDatabaseReference * parent = [self parent];
+ return parent == nil ? [self.repo description] : [NSString stringWithFormat:@"%@/%@", [parent description], [FStringUtilities urlEncoded:self.key]];
+}
+
+- (NSString *) description {
+ return [self URL];
+}
+
+- (FIRDatabaseReference *) root {
+ return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:[[FPath alloc] initWith:@""]];
+}
+
+#pragma mark -
+#pragma mark Child methods
+
+- (FIRDatabaseReference *)childByAppendingPath:(NSString *)pathString {
+ return [self child:pathString];
+}
+
+- (FIRDatabaseReference *)child:(NSString *)pathString {
+ if ([self.path getFront] == nil) {
+ // we're at the root
+ [FValidation validateFrom:@"child:" validRootPathString:pathString];
+ } else {
+ [FValidation validateFrom:@"child:" validPathString:pathString];
+ }
+ FPath* path = [self.path childFromString:pathString];
+ FIRDatabaseReference * firebaseRef = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:path];
+ return firebaseRef;
+}
+
+- (FIRDatabaseReference *) childByAutoId {
+ [FValidation validateFrom:@"childByAutoId:" writablePath:self.path];
+
+ NSString* name = [FNextPushId get:self.repo.serverTime];
+ return [self child:name];
+}
+
+#pragma mark -
+#pragma mark Basic write methods
+
+- (void) setValue:(id)value {
+ [self setValueInternal:value andPriority:nil withCompletionBlock:nil from:@"setValue:"];
+}
+
+- (void) setValue:(id)value withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self setValueInternal:value andPriority:nil withCompletionBlock:block from:@"setValue:withCompletionBlock:"];
+}
+
+- (void) setValue:(id)value andPriority:(id)priority {
+ [self setValueInternal:value andPriority:priority withCompletionBlock:nil from:@"setValue:andPriority:"];
+}
+
+- (void) setValue:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self setValueInternal:value andPriority:priority withCompletionBlock:block from:@"setValue:andPriority:withCompletionBlock:"];
+}
+
+- (void) setValueInternal:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:value priority:priority withValidationFrom:fn];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo set:self.path withNode:newNode withCallback:userCallback];
+ });
+}
+
+
+- (void) removeValue {
+ [self setValueInternal:nil andPriority:nil withCompletionBlock:nil from:@"removeValue:"];
+}
+
+- (void) removeValueWithCompletionBlock:(fbt_void_nserror_ref)block {
+ [self setValueInternal:nil andPriority:nil withCompletionBlock:block from:@"removeValueWithCompletionBlock:"];
+}
+
+
+- (void) setPriority:(id)priority {
+ [self setPriorityInternal:priority withCompletionBlock:nil from:@"setPriority:"];
+}
+
+- (void) setPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block {
+
+ [self setPriorityInternal:priority withCompletionBlock:block from:@"setPriority:withCompletionBlock:"];
+}
+
+- (void) setPriorityInternal:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo set:[self.path childFromString:@".priority"] withNode:[FSnapshotUtilities nodeFrom:priority] withCallback:userCallback];
+ });
+}
+
+
+- (void) updateChildValues:(NSDictionary *)values {
+ [self updateChildValuesInternal:values withCompletionBlock:nil from:@"updateChildValues:"];
+}
+
+- (void) updateChildValues:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self updateChildValuesInternal:values withCompletionBlock:block from:@"updateChildValues:withCompletionBlock:"];
+}
+
+- (void) updateChildValuesInternal:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ FCompoundWrite *merge = [FSnapshotUtilities compoundWriteFromDictionary:values withValidationFrom:fn];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo update:self.path withNodes:merge withCallback:userCallback];
+ });
+}
+
+#pragma mark -
+#pragma mark Disconnect Operations
+
+- (void) onDisconnectSetValue:(id)value {
+ [self onDisconnectSetValueInternal:value andPriority:nil withCompletionBlock:nil from:@"onDisconnectSetValue:"];
+}
+
+- (void) onDisconnectSetValue:(id)value withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectSetValueInternal:value andPriority:nil withCompletionBlock:block from:@"onDisconnectSetValue:withCompletionBlock:"];
+}
+
+- (void) onDisconnectSetValue:(id)value andPriority:(id)priority {
+ [self onDisconnectSetValueInternal:value andPriority:priority withCompletionBlock:nil from:@"onDisconnectSetValue:andPriority:"];
+}
+
+- (void) onDisconnectSetValue:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectSetValueInternal:value andPriority:priority withCompletionBlock:block from:@"onDisconnectSetValue:andPriority:withCompletionBlock:"];
+}
+
+- (void) onDisconnectSetValueInternal:(id)value andPriority:(id)priority withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ id<FNode> newNodeUnresolved = [FSnapshotUtilities nodeFrom:value priority:priority withValidationFrom:fn];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo onDisconnectSet:self.path withNode:newNodeUnresolved withCallback:userCallback];
+ });
+}
+
+
+- (void) onDisconnectRemoveValue {
+ [self onDisconnectSetValueInternal:nil andPriority:nil withCompletionBlock:nil from:@"onDisconnectRemoveValue:"];
+}
+
+- (void) onDisconnectRemoveValueWithCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectSetValueInternal:nil andPriority:nil withCompletionBlock:block from:@"onDisconnectRemoveValueWithCompletionBlock:"];
+}
+
+
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values {
+ [self onDisconnectUpdateChildValuesInternal:values withCompletionBlock:nil from:@"onDisconnectUpdateChildValues:"];
+}
+
+- (void) onDisconnectUpdateChildValues:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block {
+ [self onDisconnectUpdateChildValuesInternal:values withCompletionBlock:block from:@"onDisconnectUpdateChildValues:withCompletionBlock:"];
+}
+
+- (void) onDisconnectUpdateChildValuesInternal:(NSDictionary *)values withCompletionBlock:(fbt_void_nserror_ref)block from:(NSString*)fn {
+ [FValidation validateFrom:fn writablePath:self.path];
+
+ FCompoundWrite *merge = [FSnapshotUtilities compoundWriteFromDictionary:values withValidationFrom:fn];
+
+ fbt_void_nserror_ref userCallback = [block copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo onDisconnectUpdate:self.path withNodes:merge withCallback:userCallback];
+ });
+}
+
+
+- (void) cancelDisconnectOperations {
+ [self cancelDisconnectOperationsWithCompletionBlock:nil];
+}
+
+- (void) cancelDisconnectOperationsWithCompletionBlock:(fbt_void_nserror_ref)block {
+ fbt_void_nserror_ref callback = nil;
+ if (block != nil) {
+ callback = [block copy];
+ }
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo onDisconnectCancel:self.path withCallback:callback];
+ });
+}
+
+#pragma mark -
+#pragma mark Connection management methods
+
++ (void) goOffline {
+ [FRepoManager interruptAll];
+}
+
++ (void) goOnline {
+ [FRepoManager resumeAll];
+}
+
+
+#pragma mark -
+#pragma mark Data reading methods deferred to FQuery
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
+ return [self observeEventType:eventType withBlock:block withCancelBlock:nil];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+ return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ return [super observeEventType:eventType withBlock:block withCancelBlock:cancelBlock];
+}
+
+- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ return [super observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:cancelBlock];
+}
+
+
+- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle {
+ [super removeObserverWithHandle:handle];
+}
+
+
+- (void) removeAllObservers {
+ [super removeAllObservers];
+}
+
+- (void) keepSynced:(BOOL)keepSynced {
+ [super keepSynced:keepSynced];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
+ [self observeSingleEventOfType:eventType withBlock:block withCancelBlock:nil];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
+ [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [super observeSingleEventOfType:eventType withBlock:block withCancelBlock:cancelBlock];
+}
+
+- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
+ [super observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:cancelBlock];
+}
+
+#pragma mark -
+#pragma mark Query methods
+// These methods suppress warnings from having method definitions in FIRDatabaseReference.h for docs generation.
+
+- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit {
+ return [super queryLimitedToFirst:limit];
+}
+
+- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit {
+ return [super queryLimitedToLast:limit];
+}
+
+- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)key {
+ return [super queryOrderedByChild:key];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByKey {
+ return [super queryOrderedByKey];
+}
+
+- (FIRDatabaseQuery *) queryOrderedByPriority {
+ return [super queryOrderedByPriority];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue {
+ return [super queryStartingAtValue:startValue];
+}
+
+- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue childKey:(NSString *)childKey {
+ return [super queryStartingAtValue:startValue childKey:childKey];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
+ return [super queryEndingAtValue:endValue];
+}
+
+- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue childKey:(NSString *)childKey {
+ return [super queryEndingAtValue:endValue childKey:childKey];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value {
+ return [super queryEqualToValue:value];
+}
+
+- (FIRDatabaseQuery *)queryEqualToValue:(id)value childKey:(NSString *)childKey {
+ return [super queryEqualToValue:value childKey:childKey];
+}
+
+
+#pragma mark -
+#pragma mark Transaction methods
+
+- (void) runTransactionBlock:(fbt_transactionresult_mutabledata)block {
+ [FValidation validateFrom:@"runTransactionBlock:" writablePath:self.path];
+ [self runTransactionBlock:block andCompletionBlock:nil withLocalEvents:YES];
+}
+
+- (void) runTransactionBlock:(fbt_transactionresult_mutabledata)update andCompletionBlock:(fbt_void_nserror_bool_datasnapshot)completionBlock {
+ [FValidation validateFrom:@"runTransactionBlock:andCompletionBlock:" writablePath:self.path];
+ [self runTransactionBlock:update andCompletionBlock:completionBlock withLocalEvents:YES];
+}
+
+- (void) runTransactionBlock:(fbt_transactionresult_mutabledata)block andCompletionBlock:(fbt_void_nserror_bool_datasnapshot)completionBlock withLocalEvents:(BOOL)localEvents {
+ [FValidation validateFrom:@"runTransactionBlock:andCompletionBlock:withLocalEvents:" writablePath:self.path];
+ fbt_transactionresult_mutabledata updateCopy = [block copy];
+ fbt_void_nserror_bool_datasnapshot onCompleteCopy = [completionBlock copy];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.repo startTransactionOnPath:self.path update:updateCopy onComplete:onCompleteCopy withLocalEvents:localEvents];
+ });
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..ef72cf0
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary.xcodeproj/project.pbxproj
@@ -0,0 +1,438 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ EDB1C0A11653283D0041897E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0A01653283D0041897E /* Foundation.framework */; };
+ EDB1C0B01653283D0041897E /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0AF1653283D0041897E /* SenTestingKit.framework */; };
+ EDB1C0B21653283D0041897E /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0B11653283D0041897E /* UIKit.framework */; };
+ EDB1C0B31653283D0041897E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C0A01653283D0041897E /* Foundation.framework */; };
+ EDB1C0B61653283D0041897E /* libFImmutableSortedDictionary.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */; };
+ EDB1C0BC1653283D0041897E /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EDB1C0BA1653283D0041897E /* InfoPlist.strings */; };
+ EDB1C0BF1653283D0041897E /* FImmutableSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0BE1653283D0041897E /* FImmutableSortedDictionaryTests.m */; };
+ EDB1C0D21653286B0041897E /* FImmutableSortedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */; };
+ EDB1C0D31653286B0041897E /* FImmutableSortedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */; };
+ EDB1C0D41653286B0041897E /* FLLRBEmptyNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */; };
+ EDB1C0D51653286B0041897E /* FLLRBEmptyNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */; };
+ EDB1C0D61653286B0041897E /* FLLRBValueNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0D11653286B0041897E /* FLLRBValueNode.m */; };
+ EDB1C0D71653286B0041897E /* FLLRBValueNode.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0D11653286B0041897E /* FLLRBValueNode.m */; };
+ EDB1C0ED165331140041897E /* FImmutableSortedDictionaryEnumerator.m in Sources */ = {isa = PBXBuildFile; fileRef = EDB1C0EC165331140041897E /* FImmutableSortedDictionaryEnumerator.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ EDB1C0B41653283D0041897E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = EDB1C0941653283D0041897E /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = EDB1C09C1653283D0041897E;
+ remoteInfo = FImmutableSortedDictionary;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ EDB1C09B1653283D0041897E /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "include/${PRODUCT_NAME}";
+ dstSubfolderSpec = 16;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libFImmutableSortedDictionary.a; sourceTree = BUILT_PRODUCTS_DIR; };
+ EDB1C0A01653283D0041897E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+ EDB1C0A41653283D0041897E /* FImmutableSortedDictionary-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FImmutableSortedDictionary-Prefix.pch"; sourceTree = "<group>"; };
+ EDB1C0AE1653283D0041897E /* FImmutableSortedDictionaryTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FImmutableSortedDictionaryTests.octest; sourceTree = BUILT_PRODUCTS_DIR; };
+ EDB1C0AF1653283D0041897E /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; };
+ EDB1C0B11653283D0041897E /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
+ EDB1C0B91653283D0041897E /* FImmutableSortedDictionaryTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FImmutableSortedDictionaryTests-Info.plist"; sourceTree = "<group>"; };
+ EDB1C0BB1653283D0041897E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ EDB1C0BD1653283D0041897E /* FImmutableSortedDictionaryTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FImmutableSortedDictionaryTests.h; sourceTree = "<group>"; };
+ EDB1C0BE1653283D0041897E /* FImmutableSortedDictionaryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FImmutableSortedDictionaryTests.m; sourceTree = "<group>"; };
+ EDB1C0CB1653286B0041897E /* FImmutableSortedDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FImmutableSortedDictionary.h; sourceTree = "<group>"; };
+ EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FImmutableSortedDictionary.m; sourceTree = "<group>"; };
+ EDB1C0CD1653286B0041897E /* FLLRBNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLLRBNode.h; sourceTree = "<group>"; };
+ EDB1C0CE1653286B0041897E /* FLLRBEmptyNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLLRBEmptyNode.h; sourceTree = "<group>"; };
+ EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLLRBEmptyNode.m; sourceTree = "<group>"; };
+ EDB1C0D01653286B0041897E /* FLLRBValueNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLLRBValueNode.h; sourceTree = "<group>"; };
+ EDB1C0D11653286B0041897E /* FLLRBValueNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLLRBValueNode.m; sourceTree = "<group>"; };
+ EDB1C0EB165331140041897E /* FImmutableSortedDictionaryEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FImmutableSortedDictionaryEnumerator.h; sourceTree = "<group>"; };
+ EDB1C0EC165331140041897E /* FImmutableSortedDictionaryEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FImmutableSortedDictionaryEnumerator.m; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ EDB1C09A1653283D0041897E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0A11653283D0041897E /* Foundation.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ EDB1C0AA1653283D0041897E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0B01653283D0041897E /* SenTestingKit.framework in Frameworks */,
+ EDB1C0B21653283D0041897E /* UIKit.framework in Frameworks */,
+ EDB1C0B31653283D0041897E /* Foundation.framework in Frameworks */,
+ EDB1C0B61653283D0041897E /* libFImmutableSortedDictionary.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ EDB1C0921653283D0041897E = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0A21653283D0041897E /* FImmutableSortedDictionary */,
+ EDB1C0B71653283D0041897E /* FImmutableSortedDictionaryTests */,
+ EDB1C09F1653283D0041897E /* Frameworks */,
+ EDB1C09E1653283D0041897E /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ EDB1C09E1653283D0041897E /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */,
+ EDB1C0AE1653283D0041897E /* FImmutableSortedDictionaryTests.octest */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ EDB1C09F1653283D0041897E /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0A01653283D0041897E /* Foundation.framework */,
+ EDB1C0AF1653283D0041897E /* SenTestingKit.framework */,
+ EDB1C0B11653283D0041897E /* UIKit.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ EDB1C0A21653283D0041897E /* FImmutableSortedDictionary */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0CB1653286B0041897E /* FImmutableSortedDictionary.h */,
+ EDB1C0CC1653286B0041897E /* FImmutableSortedDictionary.m */,
+ EDB1C0CD1653286B0041897E /* FLLRBNode.h */,
+ EDB1C0CE1653286B0041897E /* FLLRBEmptyNode.h */,
+ EDB1C0CF1653286B0041897E /* FLLRBEmptyNode.m */,
+ EDB1C0D01653286B0041897E /* FLLRBValueNode.h */,
+ EDB1C0D11653286B0041897E /* FLLRBValueNode.m */,
+ EDB1C0EB165331140041897E /* FImmutableSortedDictionaryEnumerator.h */,
+ EDB1C0EC165331140041897E /* FImmutableSortedDictionaryEnumerator.m */,
+ EDB1C0A31653283D0041897E /* Supporting Files */,
+ );
+ path = FImmutableSortedDictionary;
+ sourceTree = "<group>";
+ };
+ EDB1C0A31653283D0041897E /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0A41653283D0041897E /* FImmutableSortedDictionary-Prefix.pch */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ EDB1C0B71653283D0041897E /* FImmutableSortedDictionaryTests */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0BD1653283D0041897E /* FImmutableSortedDictionaryTests.h */,
+ EDB1C0BE1653283D0041897E /* FImmutableSortedDictionaryTests.m */,
+ EDB1C0B81653283D0041897E /* Supporting Files */,
+ );
+ path = FImmutableSortedDictionaryTests;
+ sourceTree = "<group>";
+ };
+ EDB1C0B81653283D0041897E /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ EDB1C0B91653283D0041897E /* FImmutableSortedDictionaryTests-Info.plist */,
+ EDB1C0BA1653283D0041897E /* InfoPlist.strings */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ EDB1C09C1653283D0041897E /* FImmutableSortedDictionary */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = EDB1C0C21653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionary" */;
+ buildPhases = (
+ EDB1C0991653283D0041897E /* Sources */,
+ EDB1C09A1653283D0041897E /* Frameworks */,
+ EDB1C09B1653283D0041897E /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = FImmutableSortedDictionary;
+ productName = FImmutableSortedDictionary;
+ productReference = EDB1C09D1653283D0041897E /* libFImmutableSortedDictionary.a */;
+ productType = "com.apple.product-type.library.static";
+ };
+ EDB1C0AD1653283D0041897E /* FImmutableSortedDictionaryTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = EDB1C0C51653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionaryTests" */;
+ buildPhases = (
+ EDB1C0A91653283D0041897E /* Sources */,
+ EDB1C0AA1653283D0041897E /* Frameworks */,
+ EDB1C0AB1653283D0041897E /* Resources */,
+ EDB1C0AC1653283D0041897E /* ShellScript */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ EDB1C0B51653283D0041897E /* PBXTargetDependency */,
+ );
+ name = FImmutableSortedDictionaryTests;
+ productName = FImmutableSortedDictionaryTests;
+ productReference = EDB1C0AE1653283D0041897E /* FImmutableSortedDictionaryTests.octest */;
+ productType = "com.apple.product-type.bundle";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ EDB1C0941653283D0041897E /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0450;
+ ORGANIZATIONNAME = Firebase;
+ };
+ buildConfigurationList = EDB1C0971653283D0041897E /* Build configuration list for PBXProject "FImmutableSortedDictionary" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ );
+ mainGroup = EDB1C0921653283D0041897E;
+ productRefGroup = EDB1C09E1653283D0041897E /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ EDB1C09C1653283D0041897E /* FImmutableSortedDictionary */,
+ EDB1C0AD1653283D0041897E /* FImmutableSortedDictionaryTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ EDB1C0AB1653283D0041897E /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0BC1653283D0041897E /* InfoPlist.strings in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ EDB1C0AC1653283D0041897E /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ EDB1C0991653283D0041897E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0D21653286B0041897E /* FImmutableSortedDictionary.m in Sources */,
+ EDB1C0D41653286B0041897E /* FLLRBEmptyNode.m in Sources */,
+ EDB1C0D61653286B0041897E /* FLLRBValueNode.m in Sources */,
+ EDB1C0ED165331140041897E /* FImmutableSortedDictionaryEnumerator.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ EDB1C0A91653283D0041897E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ EDB1C0BF1653283D0041897E /* FImmutableSortedDictionaryTests.m in Sources */,
+ EDB1C0D31653286B0041897E /* FImmutableSortedDictionary.m in Sources */,
+ EDB1C0D51653286B0041897E /* FLLRBEmptyNode.m in Sources */,
+ EDB1C0D71653286B0041897E /* FLLRBValueNode.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ EDB1C0B51653283D0041897E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = EDB1C09C1653283D0041897E /* FImmutableSortedDictionary */;
+ targetProxy = EDB1C0B41653283D0041897E /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ EDB1C0BA1653283D0041897E /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ EDB1C0BB1653283D0041897E /* en */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ EDB1C0C01653283D0041897E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 6.0;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ EDB1C0C11653283D0041897E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 6.0;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ EDB1C0C31653283D0041897E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DSTROOT = /tmp/FImmutableSortedDictionary.dst;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ };
+ name = Debug;
+ };
+ EDB1C0C41653283D0041897E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DSTROOT = /tmp/FImmutableSortedDictionary.dst;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ };
+ name = Release;
+ };
+ EDB1C0C61653283D0041897E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ FRAMEWORK_SEARCH_PATHS = (
+ "\"$(SDKROOT)/Developer/Library/Frameworks\"",
+ "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ INFOPLIST_FILE = "FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = octest;
+ };
+ name = Debug;
+ };
+ EDB1C0C71653283D0041897E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ FRAMEWORK_SEARCH_PATHS = (
+ "\"$(SDKROOT)/Developer/Library/Frameworks\"",
+ "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch";
+ INFOPLIST_FILE = "FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = octest;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ EDB1C0971653283D0041897E /* Build configuration list for PBXProject "FImmutableSortedDictionary" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ EDB1C0C01653283D0041897E /* Debug */,
+ EDB1C0C11653283D0041897E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ EDB1C0C21653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionary" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ EDB1C0C31653283D0041897E /* Debug */,
+ EDB1C0C41653283D0041897E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ EDB1C0C51653283D0041897E /* Build configuration list for PBXNativeTarget "FImmutableSortedDictionaryTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ EDB1C0C61653283D0041897E /* Debug */,
+ EDB1C0C71653283D0041897E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = EDB1C0941653283D0041897E /* Project object */;
+}
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.h
new file mode 100644
index 0000000..0c6c989
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.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 "FImmutableSortedDictionary.h"
+
+/**
+ * This is an array backed implementation of FImmutableSortedDictionary. It uses arrays and linear lookups to achieve
+ * good memory efficiency while maintaining good performance for small collections. It also uses less allocations than
+ * a comparable red black tree. To avoid degrading performance with increasing collection size it will automatically
+ * convert to a FTreeSortedDictionary after an insert call above a certain threshold.
+ */
+@interface FArraySortedDictionary : FImmutableSortedDictionary
+
++ (FArraySortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator;
+
+- (id)initWithComparator:(NSComparator)comparator;
+
+#pragma mark -
+#pragma mark Properties
+
+@property (nonatomic, copy, readonly) NSComparator comparator;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m
new file mode 100644
index 0000000..f572b6b
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FArraySortedDictionary.m
@@ -0,0 +1,282 @@
+/*
+ * 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 "FArraySortedDictionary.h"
+#import "FTreeSortedDictionary.h"
+
+@interface FArraySortedDictionaryEnumerator : NSEnumerator
+
+- (id)initWithKeys:(NSArray *)keys startPos:(NSInteger)pos isReverse:(BOOL)reverse;
+- (id)nextObject;
+
+@property (nonatomic) NSInteger pos;
+@property (nonatomic) BOOL reverse;
+@property (nonatomic, strong) NSArray *keys;
+
+@end
+
+@implementation FArraySortedDictionaryEnumerator
+
+- (id)initWithKeys:(NSArray *)keys startPos:(NSInteger)pos isReverse:(BOOL)reverse
+{
+ self = [super init];
+ if (self != nil) {
+ self->_pos = pos;
+ self->_reverse = reverse;
+ self->_keys = keys;
+ }
+ return self;
+}
+
+- (id)nextObject
+{
+ NSInteger pos = self->_pos;
+ if (pos >= 0 && pos < self.keys.count) {
+ if (self.reverse) {
+ self->_pos--;
+ } else {
+ self->_pos++;
+ }
+ return self.keys[pos];
+ } else {
+ return nil;
+ }
+}
+
+@end
+
+@interface FArraySortedDictionary ()
+
+- (id)initWithComparator:(NSComparator)comparator;
+
+@property (nonatomic, copy, readwrite) NSComparator comparator;
+@property (nonatomic, strong) NSArray *keys;
+@property (nonatomic, strong) NSArray *values;
+
+@end
+
+@implementation FArraySortedDictionary
+
++ (FArraySortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator
+{
+ NSMutableArray *keys = [NSMutableArray arrayWithCapacity:dictionary.count];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ [keys addObject:key];
+ }];
+ [keys sortUsingComparator:comparator];
+
+ [keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+ if (idx > 0) {
+ if (comparator(keys[idx - 1], obj) != NSOrderedAscending) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create FImmutableSortedDictionary with keys with same ordering!"];
+ }
+ }
+ }];
+
+ NSMutableArray *values = [NSMutableArray arrayWithCapacity:keys.count];
+ NSInteger pos = 0;
+ for (id key in keys) {
+ values[pos++] = dictionary[key];
+ }
+ NSAssert(values.count == keys.count, @"We added as many keys as values");
+ return [[FArraySortedDictionary alloc] initWithComparator:comparator keys:keys values:values];
+}
+
+- (id)initWithComparator:(NSComparator)comparator
+{
+ self = [super init];
+ if (self != nil) {
+ self->_comparator = comparator;
+ self->_keys = [NSArray array];
+ self->_values = [NSArray array];
+ }
+ return self;
+}
+
+- (id)initWithComparator:(NSComparator)comparator keys:(NSArray *)keys values:(NSArray *)values
+{
+ self = [super init];
+ if (self != nil) {
+ self->_comparator = comparator;
+ self->_keys = keys;
+ self->_values = values;
+ }
+ return self;
+}
+
+- (NSInteger) findInsertPositionForKey:(id)key
+{
+ NSInteger newPos = 0;
+ while (newPos < self.keys.count && self.comparator(self.keys[newPos], key) < NSOrderedSame) {
+ newPos++;
+ }
+ return newPos;
+}
+
+- (NSInteger) findKey:(id)key
+{
+ if (key == nil) {
+ return NSNotFound;
+ }
+ for (NSInteger pos = 0; pos < self.keys.count; pos++) {
+ NSComparisonResult result = self.comparator(key, self.keys[pos]);
+ if (result == NSOrderedSame) {
+ return pos;
+ } else if (result == NSOrderedAscending) {
+ return NSNotFound;
+ }
+ }
+ return NSNotFound;
+}
+
+- (FImmutableSortedDictionary *) insertKey:(id)key withValue:(id)value
+{
+ NSInteger pos = [self findKey:key];
+
+ if (pos == NSNotFound) {
+ /*
+ * If we're above the threshold we want to convert it to a tree backed implementation to not have
+ * degrading performance
+ */
+ if (self.count >= SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD) {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:self.count];
+ for (NSInteger i = 0; i < self.keys.count; i++) {
+ dict[self.keys[i]] = self.values[i];
+ }
+ dict[key] = value;
+ return [FTreeSortedDictionary fromDictionary:dict withComparator:self.comparator];
+ } else {
+ NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys];
+ NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values];
+ NSInteger newPos = [self findInsertPositionForKey:key];
+ [newKeys insertObject:key atIndex:newPos];
+ [newValues insertObject:value atIndex:newPos];
+ return [[FArraySortedDictionary alloc] initWithComparator:self.comparator keys:newKeys values:newValues];
+ }
+ } else {
+ NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys];
+ NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values];
+ newKeys[pos] = key;
+ newValues[pos] = value;
+ return [[FArraySortedDictionary alloc] initWithComparator:self.comparator keys:newKeys values:newValues];
+ }
+}
+
+- (FImmutableSortedDictionary *) removeKey:(id)key
+{
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ return self;
+ } else {
+ NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys];
+ NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values];
+ [newKeys removeObjectAtIndex:pos];
+ [newValues removeObjectAtIndex:pos];
+ return [[FArraySortedDictionary alloc] initWithComparator:self.comparator keys:newKeys values:newValues];
+ }
+}
+
+- (id) get:(id)key
+{
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ return nil;
+ } else {
+ return self.values[pos];
+ }
+}
+
+- (id) getPredecessorKey:(id) key {
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ [NSException raise:NSInternalInconsistencyException format:@"Can't get predecessor key for non-existent key"];
+ return nil;
+ } else if (pos == 0) {
+ return nil;
+ } else {
+ return self.keys[pos - 1];
+ }
+}
+
+- (BOOL) isEmpty {
+ return self.keys.count == 0;
+}
+
+- (int) count
+{
+ return (int)self.keys.count;
+}
+
+- (id) minKey
+{
+ return [self.keys firstObject];
+}
+
+- (id) maxKey
+{
+ return [self.keys lastObject];
+}
+
+- (void) enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block
+{
+ [self enumerateKeysAndObjectsReverse:NO usingBlock:block];
+}
+
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block
+{
+ if (reverse) {
+ BOOL stop = NO;
+ for (NSInteger i = self.keys.count - 1; i >= 0; i--) {
+ block(self.keys[i], self.values[i], &stop);
+ if (stop) return;
+ }
+ } else {
+ BOOL stop = NO;
+ for (NSInteger i = 0; i < self.keys.count; i++) {
+ block(self.keys[i], self.values[i], &stop);
+ if (stop) return;
+ }
+ }
+}
+
+- (BOOL) contains:(id)key {
+ return [self findKey:key] != NSNotFound;
+}
+
+- (NSEnumerator *) keyEnumerator {
+ return [self.keys objectEnumerator];
+}
+
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey {
+ NSInteger startPos = [self findInsertPositionForKey:startKey];
+ return [[FArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys startPos:startPos isReverse:NO];
+}
+
+- (NSEnumerator *) reverseKeyEnumerator {
+ return [self.keys reverseObjectEnumerator];
+}
+
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey {
+ NSInteger startPos = [self findInsertPositionForKey:startKey];
+ // if there's no exact match, findKeyOrInsertPosition will return the index *after* the closest match, but
+ // since this is a reverse iterator, we want to start just *before* the closest match.
+ if (startPos >= self.keys.count || self.comparator(self.keys[startPos], startKey) != NSOrderedSame) {
+ startPos -= 1;
+ }
+ return [[FArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys startPos:startPos isReverse:YES];
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch
new file mode 100644
index 0000000..88d2408
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary-Prefix.pch
@@ -0,0 +1,7 @@
+//
+// Prefix header for all source files of the 'FImmutableSortedDictionary' target in the 'FImmutableSortedDictionary' project
+//
+
+#ifdef __OBJC__
+ #import <Foundation/Foundation.h>
+#endif
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.h
new file mode 100644
index 0000000..1e7e5a3
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.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.
+ */
+
+
+/**
+ * @fileoverview Implementation of an immutable SortedMap using a Left-leaning
+ * Red-Black Tree, adapted from the implementation in Mugs
+ * (http://mads379.github.com/mugs/) by Mads Hartmann Jensen
+ * (mads379@gmail.com).
+ *
+ * Original paper on Left-leaning Red-Black Trees:
+ * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
+ *
+ * Invariant 1: No red node has a red child
+ * Invariant 2: Every leaf path has the same number of black nodes
+ * Invariant 3: Only the left child can be red (left leaning)
+ */
+
+#import <Foundation/Foundation.h>
+
+/**
+ * The size threshold where we use a tree backed sorted map instead of an array backed sorted map.
+ * This is a more or less arbitrary chosen value, that was chosen to be large enough to fit most of object kind
+ * of Firebase data, but small enough to not notice degradation in performance for inserting and lookups.
+ * Feel free to empirically determine this constant, but don't expect much gain in real world performance.
+ */
+#define SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD 25
+
+@interface FImmutableSortedDictionary : NSObject
+
++ (FImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator;
++ (FImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator;
+
+- (FImmutableSortedDictionary *) insertKey:(id)aKey withValue:(id)aValue;
+- (FImmutableSortedDictionary *) removeKey:(id)aKey;
+- (id) get:(id) key;
+- (id) getPredecessorKey:(id) key;
+- (BOOL) isEmpty;
+- (int) count;
+- (id) minKey;
+- (id) maxKey;
+- (void) enumerateKeysAndObjectsUsingBlock:(void(^)(id key, id value, BOOL *stop))block;
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void(^)(id key, id value, BOOL *stop))block;
+- (BOOL) contains:(id)key;
+- (NSEnumerator *) keyEnumerator;
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey;
+- (NSEnumerator *) reverseKeyEnumerator;
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey;
+
+#pragma mark -
+#pragma mark Methods similar to NSMutableDictionary
+
+- (FImmutableSortedDictionary *) setObject:(id)anObject forKey:(id)aKey;
+- (id) objectForKey:(id)key;
+- (FImmutableSortedDictionary *) removeObjectForKey:(id)aKey;
+
+@end
+
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m
new file mode 100644
index 0000000..006c12d
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedDictionary.m
@@ -0,0 +1,158 @@
+/*
+ * 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 "FArraySortedDictionary.h"
+#import "FTreeSortedDictionary.h"
+
+#define THROW_ABSTRACT_METHOD_EXCEPTION(sel) do { \
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException \
+ reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(sel)] \
+ userInfo:nil]; \
+} while(0)
+
+@implementation FImmutableSortedDictionary
+
++ (FImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator
+{
+ return [[FArraySortedDictionary alloc] initWithComparator:comparator];
+}
+
++ (FImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator
+{
+ if (dictionary.count <= SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD) {
+ return [FArraySortedDictionary fromDictionary:dictionary withComparator:comparator];
+ } else {
+ return [FTreeSortedDictionary fromDictionary:dictionary withComparator:comparator];
+ }
+}
+
+- (FImmutableSortedDictionary *) insertKey:(id)aKey withValue:(id)aValue {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(insertKey:withValue:));
+}
+
+- (FImmutableSortedDictionary *) removeKey:(id)aKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(removeKey:));
+}
+
+- (id) get:(id) key {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(get:));
+}
+
+- (id) getPredecessorKey:(id) key {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(getPredecessorKey:));
+}
+
+- (BOOL) isEmpty {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(isEmpty));
+}
+
+- (int) count {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector((count)));
+}
+
+- (id) minKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(minKey));
+}
+
+- (id) maxKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(maxKey));
+}
+
+- (void) enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(enumerateKeysAndObjectsUsingBlock:));
+}
+
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(enumerateKeysAndObjectsReverse:usingBlock:));
+}
+
+- (BOOL) contains:(id)key {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(contains:));
+}
+
+- (NSEnumerator *) keyEnumerator {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(keyEnumerator));
+}
+
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(keyEnumeratorFrom:));
+}
+
+- (NSEnumerator *) reverseKeyEnumerator {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(reverseKeyEnumerator));
+}
+
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey {
+ THROW_ABSTRACT_METHOD_EXCEPTION(@selector(reverseKeyEnumeratorFrom:));
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FImmutableSortedDictionary class]]) {
+ return NO;
+ }
+ FImmutableSortedDictionary *other = (FImmutableSortedDictionary *)object;
+ if (self.count != other.count) {
+ return NO;
+ }
+ __block BOOL isEqual = YES;
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ id otherValue = [other objectForKey:key];
+ isEqual = isEqual && (value == otherValue || [value isEqual:otherValue]);
+ *stop = !isEqual;
+ }];
+ return isEqual;
+}
+
+- (NSUInteger)hash {
+ __block NSUInteger hash = 0;
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ hash = (hash * 31 + [key hash]) * 17 + [value hash];
+ }];
+ return hash;
+}
+
+- (NSString *)description {
+ NSMutableString *str = [[NSMutableString alloc] init];
+ __block BOOL first = YES;
+ [str appendString:@"{ "];
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ if (!first) {
+ [str appendString:@", "];
+ }
+ first = NO;
+ [str appendString:[NSString stringWithFormat:@"%@: %@", key, value]];
+ }];
+ [str appendString:@" }"];
+ return str;
+}
+
+#pragma mark -
+#pragma mark Methods similar to NSMutableDictionary
+
+- (FImmutableSortedDictionary *) setObject:(__unsafe_unretained id)anObject forKey:(__unsafe_unretained id)aKey {
+ return [self insertKey:aKey withValue:anObject];
+}
+
+- (FImmutableSortedDictionary *) removeObjectForKey:(__unsafe_unretained id)aKey {
+ return [self removeKey:aKey];
+}
+
+- (id) objectForKey:(__unsafe_unretained id)key {
+ return [self get:key];
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.h
new file mode 100644
index 0000000..bb1a39c
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.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>
+
+@interface FImmutableSortedSet : NSObject
+
++ (FImmutableSortedSet *)setWithKeysFromDictionary:(NSDictionary *)array comparator:(NSComparator)comparator;
+
+- (BOOL)containsObject:(id)object;
+- (FImmutableSortedSet *)addObject:(id)object;
+- (FImmutableSortedSet *)removeObject:(id)object;
+- (id)firstObject;
+- (id)lastObject;
+- (NSUInteger)count;
+- (BOOL)isEmpty;
+
+- (id)predecessorEntry:(id)entry;
+
+- (void)enumerateObjectsUsingBlock:(void (^)(id obj, BOOL *stop))block;
+- (void)enumerateObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id obj, BOOL *stop))block;
+
+- (NSEnumerator *)objectEnumerator;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.m
new file mode 100644
index 0000000..09c4164
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FImmutableSortedSet.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 "FImmutableSortedSet.h"
+#import "FImmutableSortedDictionary.h"
+
+@interface FImmutableSortedSet ()
+
+@property (nonatomic, strong) FImmutableSortedDictionary *dictionary;
+
+@end
+
+@implementation FImmutableSortedSet
+
++ (FImmutableSortedSet *)setWithKeysFromDictionary:(NSDictionary *)dictionary comparator:(NSComparator)comparator
+{
+ FImmutableSortedDictionary *setDict = [FImmutableSortedDictionary fromDictionary:dictionary withComparator:comparator];
+ return [[FImmutableSortedSet alloc] initWithDictionary:setDict];
+}
+
+- (id)initWithDictionary:(FImmutableSortedDictionary *)dictionary
+{
+ self = [super init];
+ if (self != nil) {
+ self->_dictionary = dictionary;
+ }
+ return self;
+}
+
+- (BOOL)contains:(id)object
+{
+ return [self.dictionary contains:object];
+}
+
+- (FImmutableSortedSet *)addObject:(id)object
+{
+ FImmutableSortedDictionary *newDictionary = [self.dictionary insertKey:object withValue:[NSNull null]];
+ if (newDictionary != self.dictionary) {
+ return [[FImmutableSortedSet alloc] initWithDictionary:newDictionary];
+ } else {
+ return self;
+ }
+}
+
+- (FImmutableSortedSet *)removeObject:(id)object
+{
+ FImmutableSortedDictionary *newDictionary = [self.dictionary removeObjectForKey:object];
+ if (newDictionary != self.dictionary) {
+ return [[FImmutableSortedSet alloc] initWithDictionary:newDictionary];
+ } else {
+ return self;
+ }
+}
+
+- (BOOL)containsObject:(id)object
+{
+ return [self.dictionary contains:object];
+}
+
+- (id)firstObject
+{
+ return [self.dictionary minKey];
+}
+
+- (id)lastObject
+{
+ return [self.dictionary maxKey];
+}
+
+- (id)predecessorEntry:(id)entry
+{
+ return [self.dictionary getPredecessorKey:entry];
+}
+
+- (NSUInteger)count
+{
+ return [self.dictionary count];
+}
+
+- (BOOL)isEmpty
+{
+ return [self.dictionary isEmpty];
+}
+
+- (void)enumerateObjectsUsingBlock:(void (^)(id, BOOL *))block
+{
+ [self enumerateObjectsReverse:NO usingBlock:block];
+}
+
+- (void)enumerateObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, BOOL *))block
+{
+ [self.dictionary enumerateKeysAndObjectsReverse:reverse usingBlock:^(id key, id value, BOOL *stop) {
+ block(key, stop);
+ }];
+}
+
+- (NSEnumerator *)objectEnumerator
+{
+ return [self.dictionary keyEnumerator];
+}
+
+- (NSString *)description
+{
+ NSMutableString *str = [[NSMutableString alloc] init];
+ __block BOOL first = YES;
+ [str appendString:@"FImmutableSortedSet ( "];
+ [self enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
+ if (!first) {
+ [str appendString:@", "];
+ }
+ first = NO;
+ [str appendString:[NSString stringWithFormat:@"%@", obj]];
+ }];
+ [str appendString:@" )"];
+ return str;
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h
new file mode 100644
index 0000000..3257447
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "FLLRBNode.h"
+
+@interface FLLRBEmptyNode : NSObject <FLLRBNode>
+
++ (id)emptyNode;
+
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight;
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+- (id<FLLRBNode>) remove:(id) aKey withComparator:(NSComparator)aComparator;
+- (int) count;
+- (BOOL) isEmpty;
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action;
+- (id<FLLRBNode>) min;
+- (id) minKey;
+- (id) maxKey;
+- (BOOL) isRed;
+- (int) check;
+
+@property (nonatomic, strong) id key;
+@property (nonatomic, strong) id value;
+@property (nonatomic, strong) FLLRBColor* color;
+@property (nonatomic, strong) id<FLLRBNode> left;
+@property (nonatomic, strong) id<FLLRBNode> right;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m
new file mode 100644
index 0000000..adbc6ca
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBEmptyNode.m
@@ -0,0 +1,87 @@
+/*
+ * 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 "FLLRBEmptyNode.h"
+#import "FLLRBValueNode.h"
+
+@implementation FLLRBEmptyNode
+
+@synthesize key, value, color, left, right;
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"[key=%@ val=%@ color=%@]", key, value, (color ? @"true" : @"false")];
+}
+
++ (id)emptyNode
+{
+ static dispatch_once_t pred = 0;
+ __strong static id _sharedObject = nil;
+ dispatch_once(&pred, ^{
+ _sharedObject = [[self alloc] init]; // or some other init method
+ });
+ return _sharedObject;
+}
+
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight {
+ return self;
+}
+
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator {
+ FLLRBValueNode* result = [[FLLRBValueNode alloc] initWithKey:aKey withValue:aValue withColor:nil withLeft:nil withRight:nil];
+ return result;
+}
+
+- (id<FLLRBNode>) remove:(id) key withComparator:(NSComparator)aComparator {
+ return self;
+}
+
+- (int) count {
+ return 0;
+}
+
+- (BOOL) isEmpty {
+ return YES;
+}
+
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action {
+ return NO;
+}
+
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action {
+ return NO;
+}
+
+- (id<FLLRBNode>) min {
+ return self;
+}
+
+- (id) minKey {
+ return nil;
+}
+
+- (id) maxKey {
+ return nil;
+}
+
+- (BOOL) isRed {
+ return NO;
+}
+
+- (int) check {
+ return 0;
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.h
new file mode 100644
index 0000000..2634494
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBNode.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>
+
+#define RED @true
+#define BLACK @false
+
+typedef NSNumber FLLRBColor;
+
+@protocol FLLRBNode <NSObject>
+
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight;
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+- (id<FLLRBNode>) remove:(id) key withComparator:(NSComparator)aComparator;
+- (int) count;
+- (BOOL) isEmpty;
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action;
+- (id<FLLRBNode>) min;
+- (id) minKey;
+- (id) maxKey;
+- (BOOL) isRed;
+- (int) check;
+
+@property (nonatomic, strong) id key;
+@property (nonatomic, strong) id value;
+@property (nonatomic, strong) FLLRBColor* color;
+@property (nonatomic, strong) id<FLLRBNode> left;
+@property (nonatomic, strong) id<FLLRBNode> right;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.h
new file mode 100644
index 0000000..2c64b8a
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.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>
+#import "FLLRBNode.h"
+
+@interface FLLRBValueNode : NSObject <FLLRBNode>
+
+
+- (id)initWithKey:(id) key withValue:(id) value withColor:(FLLRBColor*) color withLeft:(id<FLLRBNode>)left withRight:(id<FLLRBNode>)right;
+- (id)copyWith:(id) aKey withValue:(id) aValue withColor:(FLLRBColor*) aColor withLeft:(id<FLLRBNode>)aLeft withRight:(id<FLLRBNode>)aRight;
+- (id<FLLRBNode>) insertKey:(id) aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+- (id<FLLRBNode>) remove:(id) aKey withComparator:(NSComparator)aComparator;
+- (int) count;
+- (BOOL) isEmpty;
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action;
+- (id<FLLRBNode>) min;
+- (id) minKey;
+- (id) maxKey;
+- (BOOL) isRed;
+- (int) check;
+
+- (BOOL) checkMaxDepth;
+
+@property (nonatomic, strong) id key;
+@property (nonatomic, strong) id value;
+@property (nonatomic, strong) FLLRBColor* color;
+@property (nonatomic, strong) id<FLLRBNode> left;
+@property (nonatomic, strong) id<FLLRBNode> right;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m
new file mode 100644
index 0000000..f361278
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FLLRBValueNode.m
@@ -0,0 +1,245 @@
+/*
+ * 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 "FLLRBValueNode.h"
+#import "FLLRBEmptyNode.h"
+
+@implementation FLLRBValueNode
+
+@synthesize key, value, color, left, right;
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"[key=%@ val=%@ color=%@]", key, value, (color ? @"true" : @"false")];
+}
+
+- (id)initWithKey:(__unsafe_unretained id) aKey withValue:(__unsafe_unretained id) aValue withColor:(__unsafe_unretained FLLRBColor*) aColor withLeft:(__unsafe_unretained id<FLLRBNode>)aLeft withRight:(__unsafe_unretained id<FLLRBNode>)aRight
+{
+ self = [super init];
+ if (self) {
+ self.key = aKey;
+ self.value = aValue;
+ self.color = aColor != nil ? aColor : RED;
+ self.left = aLeft != nil ? aLeft : [FLLRBEmptyNode emptyNode];
+ self.right = aRight != nil ? aRight : [FLLRBEmptyNode emptyNode];
+ }
+ return self;
+}
+
+- (id)copyWith:(__unsafe_unretained id) aKey withValue:(__unsafe_unretained id) aValue withColor:(__unsafe_unretained FLLRBColor*) aColor withLeft:(__unsafe_unretained id<FLLRBNode>)aLeft withRight:(__unsafe_unretained id<FLLRBNode>)aRight {
+ return [[FLLRBValueNode alloc] initWithKey:(aKey != nil) ? aKey : self.key
+ withValue:(aValue != nil) ? aValue : self.value
+ withColor:(aColor != nil) ? aColor : self.color
+ withLeft:(aLeft != nil) ? aLeft : self.left
+ withRight:(aRight != nil) ? aRight : self.right];
+}
+
+- (int) count {
+ return [self.left count] + 1 + [self.right count];
+}
+
+- (BOOL) isEmpty {
+ return NO;
+}
+
+/**
+* Early terminates if aciton returns YES.
+* @return The first truthy value returned by action, or the last falsey value returned by action.
+*/
+- (BOOL) inorderTraversal:(BOOL (^)(id key, id value))action {
+ return [self.left inorderTraversal:action] ||
+ action(self.key, self.value) ||
+ [self.right inorderTraversal:action];
+}
+
+- (BOOL) reverseTraversal:(BOOL (^)(id key, id value))action {
+ return [self.right reverseTraversal:action] ||
+ action(self.key, self.value) ||
+ [self.left reverseTraversal:action];
+}
+
+- (id<FLLRBNode>) min {
+ if([self.left isEmpty]) {
+ return self;
+ }
+ else {
+ return [self.left min];
+ }
+}
+
+- (id) minKey {
+ return [[self min] key];
+}
+
+- (id) maxKey {
+ if([self.right isEmpty]) {
+ return self.key;
+ }
+ else {
+ return [self.right maxKey];
+ }
+}
+
+- (id<FLLRBNode>) insertKey:(__unsafe_unretained id) aKey forValue:(__unsafe_unretained id)aValue withComparator:(NSComparator)aComparator {
+ NSComparisonResult cmp = aComparator(aKey, self.key);
+ FLLRBValueNode* n = self;
+
+ if(cmp == NSOrderedAscending) {
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:[n.left insertKey:aKey forValue:aValue withComparator:aComparator] withRight:nil];
+ }
+ else if(cmp == NSOrderedSame) {
+ n = [n copyWith:nil withValue:aValue withColor:nil withLeft:nil withRight:nil];
+ }
+ else {
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:nil withRight:[n.right insertKey:aKey forValue:aValue withComparator:aComparator]];
+ }
+
+ return [n fixUp];
+}
+
+- (id<FLLRBNode>) removeMin {
+
+ if([self.left isEmpty]) {
+ return [FLLRBEmptyNode emptyNode];
+ }
+
+ FLLRBValueNode* n = self;
+ if(! [n.left isRed] && ! [n.left.left isRed]) {
+ n = [n moveRedLeft];
+ }
+
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:[(FLLRBValueNode*)n.left removeMin] withRight:nil];
+ return [n fixUp];
+}
+
+
+- (id<FLLRBNode>) fixUp {
+ FLLRBValueNode* n = self;
+ if([n.right isRed] && ! [n.left isRed]) n = [n rotateLeft];
+ if([n.left isRed] && [n.left.left isRed]) n = [n rotateRight];
+ if([n.left isRed] && [n.right isRed]) n = [n colorFlip];
+ return n;
+}
+
+- (FLLRBValueNode*) moveRedLeft {
+ FLLRBValueNode* n = [self colorFlip];
+ if([n.right.left isRed]) {
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:nil withRight:[(FLLRBValueNode*)n.right rotateRight]];
+ n = [n rotateLeft];
+ n = [n colorFlip];
+ }
+ return n;
+}
+
+- (FLLRBValueNode*) moveRedRight {
+ FLLRBValueNode* n = [self colorFlip];
+ if([n.left.left isRed]) {
+ n = [n rotateRight];
+ n = [n colorFlip];
+ }
+ return n;
+}
+
+- (id<FLLRBNode>) rotateLeft {
+ id<FLLRBNode> nl = [self copyWith:nil withValue:nil withColor:RED withLeft:nil withRight:self.right.left];
+ return [self.right copyWith:nil withValue:nil withColor:self.color withLeft:nl withRight:nil];;
+}
+
+- (id<FLLRBNode>) rotateRight {
+ id<FLLRBNode> nr = [self copyWith:nil withValue:nil withColor:RED withLeft:self.left.right withRight:nil];
+ return [self.left copyWith:nil withValue:nil withColor:self.color withLeft:nil withRight:nr];
+}
+
+- (id<FLLRBNode>) colorFlip {
+ id<FLLRBNode> nleft = [self.left copyWith:nil withValue:nil withColor:[NSNumber numberWithBool:![self.left.color boolValue]] withLeft:nil withRight:nil];
+ id<FLLRBNode> nright = [self.right copyWith:nil withValue:nil withColor:[NSNumber numberWithBool:![self.right.color boolValue]] withLeft:nil withRight:nil];
+
+ return [self copyWith:nil withValue:nil withColor:[NSNumber numberWithBool:![self.color boolValue]] withLeft:nleft withRight:nright];
+}
+
+- (id<FLLRBNode>) remove:(__unsafe_unretained id) aKey withComparator:(NSComparator)comparator {
+ id<FLLRBNode> smallest;
+ FLLRBValueNode* n = self;
+
+ if(comparator(aKey, n.key) == NSOrderedAscending) {
+ if(![n.left isEmpty] && ![n.left isRed] && ![n.left.left isRed]) {
+ n = [n moveRedLeft];
+ }
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:[n.left remove:aKey withComparator:comparator] withRight:nil];
+ }
+ else {
+ if([n.left isRed]) {
+ n = [n rotateRight];
+ }
+
+ if(![n.right isEmpty] && ![n.right isRed] && ![n.right.left isRed]) {
+ n = [n moveRedRight];
+ }
+
+ if(comparator(aKey, n.key) == NSOrderedSame) {
+ if([n.right isEmpty]) {
+ return [FLLRBEmptyNode emptyNode];
+ }
+ else {
+ smallest = [n.right min];
+ n = [n copyWith:smallest.key withValue:smallest.value withColor:nil withLeft:nil withRight:[(FLLRBValueNode*)n.right removeMin]];
+ }
+ }
+ n = [n copyWith:nil withValue:nil withColor:nil withLeft:nil withRight:[n.right remove:aKey withComparator:comparator]];
+ }
+ return [n fixUp];
+}
+
+- (BOOL) isRed {
+ return [self.color boolValue];
+}
+
+- (BOOL) checkMaxDepth {
+ int blackDepth = [self check];
+ if(pow(2.0, blackDepth) <= ([self count] + 1)) {
+ return YES;
+ }
+ else {
+ return NO;
+ }
+}
+
+- (int) check {
+ int blackDepth = 0;
+
+ if([self isRed] && [self.left isRed]) {
+ @throw [[NSException alloc] initWithName:@"check" reason:@"Red node has a red child" userInfo:nil];
+ }
+
+ if([self.right isRed]) {
+ @throw [[NSException alloc] initWithName:@"check" reason:@"Right child is red" userInfo:nil];
+ }
+
+ blackDepth = [self.left check];
+// NSLog(err);
+ if(blackDepth != [self.right check]) {
+ NSString* err = [NSString stringWithFormat:@"(%@ -> %@)blackDepth: %d ; self.right check: %d", self.value, [self.color boolValue] ? @"red" : @"black", blackDepth, [self.right check]];
+// return 10;
+ @throw [[NSException alloc] initWithName:@"check" reason:err userInfo:nil];
+ }
+ else {
+ int ret = blackDepth + ([self isRed] ? 0 : 1);
+// NSLog(@"black depth is: %d; other is: %d, ret is: %d", blackDepth, ([self isRed] ? 0 : 1), ret);
+ return ret;
+ }
+}
+
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.h
new file mode 100644
index 0000000..d7fe835
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.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>
+#import "FTreeSortedDictionary.h"
+
+@interface FTreeSortedDictionaryEnumerator : NSEnumerator
+
+- (id)initWithImmutableSortedDictionary:(FTreeSortedDictionary *)aDict startKey:(id)startKey isReverse:(BOOL)reverse;
+- (id)nextObject;
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m
new file mode 100644
index 0000000..6636d1e
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionary/FTreeSortedDictionaryEnumerator.m
@@ -0,0 +1,99 @@
+/*
+ * 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 "FTreeSortedDictionaryEnumerator.h"
+
+@interface FTreeSortedDictionaryEnumerator()
+@property (nonatomic, strong) FTreeSortedDictionary* immutableSortedDictionary;
+@property (nonatomic, strong) NSMutableArray* stack;
+@property (nonatomic) BOOL isReverse;
+
+@end
+
+@implementation FTreeSortedDictionaryEnumerator
+
+- (id)initWithImmutableSortedDictionary:(FTreeSortedDictionary *)aDict
+ startKey:(id)startKey isReverse:(BOOL)reverse {
+ self = [super init];
+ if (self) {
+ self.immutableSortedDictionary = aDict;
+ self.stack = [[NSMutableArray alloc] init];
+ self.isReverse = reverse;
+
+ NSComparator comparator = aDict.comparator;
+ id<FLLRBNode> node = self.immutableSortedDictionary.root;
+
+ NSInteger cmp;
+ while(![node isEmpty]) {
+ cmp = startKey ? comparator(node.key, startKey) : 1;
+ // flip the comparison if we're going in reverse
+ if (self.isReverse) cmp *= -1;
+
+ if (cmp < 0) {
+ // This node is less than our start key. Ignore it.
+ if (self.isReverse) {
+ node = node.left;
+ } else {
+ node = node.right;
+ }
+ } else if (cmp == 0) {
+ // This node is exactly equal to our start key. Push it on the stack, but stop iterating:
+ [self.stack addObject:node];
+ break;
+ } else {
+ // This node is greater than our start key, add it to the stack and move on to the next one.
+ [self.stack addObject:node];
+ if (self.isReverse) {
+ node = node.right;
+ } else {
+ node = node.left;
+ }
+ }
+ }
+ }
+ return self;
+}
+
+- (id)nextObject {
+ if([self.stack count] == 0) {
+ return nil;
+ }
+
+ id<FLLRBNode> node = nil;
+ @synchronized(self.stack) {
+ node = [self.stack lastObject];
+ [self.stack removeLastObject];
+ }
+ id result = node.key;
+
+ if (self.isReverse) {
+ node = node.left;
+ while (![node isEmpty]) {
+ [self.stack addObject:node];
+ node = node.right;
+ }
+ } else {
+ node = node.right;
+ while (![node isEmpty]) {
+ [self.stack addObject:node];
+ node = node.left;
+ }
+ }
+
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist
new file mode 100644
index 0000000..42887ee
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/FImmutableSortedDictionaryTests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.firebase.mobile.ios.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/Firebase/Database/FImmutableSortedDictionary/FImmutableSortedDictionaryTests/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/Firebase/Database/FIndex.h b/Firebase/Database/FIndex.h
new file mode 100644
index 0000000..8ab08c8
--- /dev/null
+++ b/Firebase/Database/FIndex.h
@@ -0,0 +1,50 @@
+/*
+ * 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 FImmutableSortedDictionary;
+@class FNamedNode;
+@protocol FNode;
+
+@protocol FIndex<NSObject, NSCopying>
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2;
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse;
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2;
+
+- (BOOL) isDefinedOn:(id<FNode>)node;
+- (BOOL) indexedValueChangedBetween:(id<FNode>)oldNode and:(id<FNode>)newNode;
+- (FNamedNode*) minPost;
+- (FNamedNode*) maxPost;
+- (FNamedNode*) makePost:(id<FNode>)indexValue name:(NSString*)name;
+- (NSString*) queryDefinition;
+
+@end
+
+@interface FIndex : NSObject
+
++ (id<FIndex>)indexFromQueryDefinition:(NSString *)string;
+
+@end
diff --git a/Firebase/Database/FIndex.m b/Firebase/Database/FIndex.m
new file mode 100644
index 0000000..61980c7
--- /dev/null
+++ b/Firebase/Database/FIndex.m
@@ -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 "FIndex.h"
+
+#import "FKeyIndex.h"
+#import "FValueIndex.h"
+#import "FPathIndex.h"
+#import "FPriorityIndex.h"
+
+@implementation FIndex
+
++ (id<FIndex>)indexFromQueryDefinition:(NSString *)string {
+ if ([string isEqualToString:@".key"]) {
+ return [FKeyIndex keyIndex];
+ } else if ([string isEqualToString:@".value"]) {
+ return [FValueIndex valueIndex];
+ } else if ([string isEqualToString:@".priority"]) {
+ return [FPriorityIndex priorityIndex];
+ } else {
+ return [[FPathIndex alloc] initWithPath:[[FPath alloc] initWith:string]];
+ }
+}
+
+@end
diff --git a/Firebase/Database/FKeyIndex.h b/Firebase/Database/FKeyIndex.h
new file mode 100644
index 0000000..a6bf787
--- /dev/null
+++ b/Firebase/Database/FKeyIndex.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "FIndex.h"
+
+
+@interface FKeyIndex : NSObject<FIndex>
++ (id<FIndex>) keyIndex;
+@end
diff --git a/Firebase/Database/FKeyIndex.m b/Firebase/Database/FKeyIndex.m
new file mode 100644
index 0000000..68ad461
--- /dev/null
+++ b/Firebase/Database/FKeyIndex.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 "FKeyIndex.h"
+#import "FNamedNode.h"
+#import "FSnapshotUtilities.h"
+#import "FUtilities.h"
+#import "FEmptyNode.h"
+
+@interface FKeyIndex ()
+
+@property (nonatomic, strong) FNamedNode *maxPost;
+
+@end
+
+@implementation FKeyIndex
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ self.maxPost = [[FNamedNode alloc] initWithName:[FUtilities maxName] andNode:[FEmptyNode emptyNode]];
+ }
+ return self;
+
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ return [FUtilities compareKey:key1 toKey:key2];
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id <FNode>)node {
+ return YES;
+}
+
+- (BOOL)indexedValueChangedBetween:(id <FNode>)oldNode and:(id <FNode>)newNode {
+ return NO; // The key for a node never changes.
+}
+
+- (FNamedNode *)minPost {
+ return [FNamedNode min];
+}
+
+- (FNamedNode *)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ NSString *key = indexValue.val;
+ NSAssert([key isKindOfClass:[NSString class]], @"KeyIndex indexValue must always be a string.");
+ // We just use empty node, but it'll never be compared, since our comparator only looks at name.
+ return [[FNamedNode alloc] initWithName:key andNode:[FEmptyNode emptyNode]];
+}
+
+- (NSString *) queryDefinition {
+ return @".key";
+}
+
+- (NSString *) description {
+ return @"FKeyIndex";
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ // since we're a singleton.
+ return (other == self);
+}
+
+- (NSUInteger) hash {
+ return [@".key" hash];
+}
+
+
++ (id<FIndex>) keyIndex {
+ static id<FIndex> keyIndex;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ keyIndex = [[FKeyIndex alloc] init];
+ });
+ return keyIndex;
+}
+@end
diff --git a/Firebase/Database/FListenComplete.h b/Firebase/Database/FListenComplete.h
new file mode 100644
index 0000000..914a3e4
--- /dev/null
+++ b/Firebase/Database/FListenComplete.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "FOperation.h"
+
+
+@interface FListenComplete : NSObject <FOperation>
+
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath;
+
+@property (nonatomic, strong, readonly) FOperationSource *source;
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, readonly) FOperationType type;
+
+@end
diff --git a/Firebase/Database/FListenComplete.m b/Firebase/Database/FListenComplete.m
new file mode 100644
index 0000000..8573075
--- /dev/null
+++ b/Firebase/Database/FListenComplete.m
@@ -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 "FListenComplete.h"
+#import "FOperationSource.h"
+#import "FPath.h"
+
+@interface FListenComplete ()
+@property (nonatomic, strong, readwrite) FOperationSource *source;
+@property (nonatomic, strong, readwrite) FPath *path;
+@property (nonatomic, readwrite) FOperationType type;
+@end
+
+@implementation FListenComplete
+- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath {
+ NSAssert(!aSource.fromUser, @"Can't have a listen complete from a user source");
+ self = [super init];
+ if (self) {
+ self.source = aSource;
+ self.path = aPath;
+ self.type = FOperationTypeListenComplete;
+ }
+ return self;
+}
+
+- (id <FOperation>) operationForChild:(NSString *)childKey {
+ if ([self.path isEmpty]) {
+ return [[FListenComplete alloc] initWithSource:self.source path:[FPath empty]];
+ } else {
+ return [[FListenComplete alloc] initWithSource:self.source path:[self.path popFront]];
+ }
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"FListenComplete { path=%@, source=%@ }", self.path, self.source];
+}
+
+@end
diff --git a/Firebase/Database/FMaxNode.h b/Firebase/Database/FMaxNode.h
new file mode 100644
index 0000000..6aff8c6
--- /dev/null
+++ b/Firebase/Database/FMaxNode.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "FChildrenNode.h"
+
+
+@interface FMaxNode : FChildrenNode
+ + (id<FNode>) maxNode;
+@end
diff --git a/Firebase/Database/FMaxNode.m b/Firebase/Database/FMaxNode.m
new file mode 100644
index 0000000..3c93684
--- /dev/null
+++ b/Firebase/Database/FMaxNode.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 "FMaxNode.h"
+#import "FUtilities.h"
+#import "FEmptyNode.h"
+
+
+@implementation FMaxNode {
+
+}
+- (id) init {
+ self = [super init];
+ if (self) {
+
+ }
+ return self;
+}
+
++ (id<FNode>) maxNode {
+ static FMaxNode *maxNode = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ maxNode = [[FMaxNode alloc] init];
+ });
+ return maxNode;
+}
+
+- (NSComparisonResult) compare:(id<FNode>)other {
+ if (other == self) {
+ return NSOrderedSame;
+ } else {
+ return NSOrderedDescending;
+ }
+}
+
+- (BOOL)isEqual:(id)other {
+ return other == self;
+}
+
+- (id<FNode>) getImmediateChild:(NSString *) childName {
+ return [FEmptyNode emptyNode];
+}
+
+- (BOOL) isEmpty {
+ return NO;
+}
+@end
diff --git a/Firebase/Database/FNamedNode.h b/Firebase/Database/FNamedNode.h
new file mode 100644
index 0000000..ac9baa6
--- /dev/null
+++ b/Firebase/Database/FNamedNode.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 "FNode.h"
+
+@interface FNamedNode : NSObject<NSCopying>
+
+@property (nonatomic, strong, readonly) NSString* name;
+@property (nonatomic, strong, readonly) id<FNode> node;
+
+
+-(id)initWithName:(NSString*)name andNode:(id<FNode>)node;
+
++ (FNamedNode *)nodeWithName:(NSString *)name node:(id<FNode>)node;
+
++ (FNamedNode*) min;
++ (FNamedNode*) max;
+@end
diff --git a/Firebase/Database/FNamedNode.m b/Firebase/Database/FNamedNode.m
new file mode 100644
index 0000000..d11787b
--- /dev/null
+++ b/Firebase/Database/FNamedNode.m
@@ -0,0 +1,94 @@
+/*
+ * 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 "FNamedNode.h"
+#import "FUtilities.h"
+#import "FEmptyNode.h"
+#import "FMaxNode.h"
+#import "FIndex.h"
+
+@interface FNamedNode ()
+@property (nonatomic, strong, readwrite) NSString* name;
+@property (nonatomic, strong, readwrite) id<FNode> node;
+@end
+
+@implementation FNamedNode
+
++ (FNamedNode *)nodeWithName:(NSString *)name node:(id<FNode>)node
+{
+ return [[FNamedNode alloc] initWithName:name andNode:node];
+}
+
+- (id)initWithName:(NSString *)name andNode:(id <FNode>)node {
+ self = [super init];
+ if (self) {
+ self.name = name;
+ self.node = node;
+ }
+ return self;
+}
+
+- (id)copy
+{
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone
+{
+ return self;
+}
+
++ (FNamedNode *)min {
+ static FNamedNode *min = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ min = [[FNamedNode alloc] initWithName:[FUtilities minName] andNode:[FEmptyNode emptyNode]];
+ });
+ return min;
+}
+
++ (FNamedNode *)max {
+ static FNamedNode *max = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ max = [[FNamedNode alloc] initWithName:[FUtilities maxName] andNode:[FMaxNode maxNode]];
+ });
+ return max;
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"NamedNode[%@] %@", self.name, self.node];
+}
+
+- (BOOL) isEqual:(id)object {
+ if (self == object) { return YES; }
+ if (object == nil || ![object isKindOfClass:[FNamedNode class]]) { return NO; }
+
+ FNamedNode *namedNode = object;
+ if (![self.name isEqualToString:namedNode.name]) { return NO; }
+ if (![self.node isEqual:namedNode.node]) { return NO; }
+
+ return YES;
+}
+
+- (NSUInteger) hash {
+ NSUInteger nameHash = [self.name hash];
+ NSUInteger nodeHash = [self.node hash];
+ NSUInteger result = 31 * nameHash + nodeHash;
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/FPathIndex.h b/Firebase/Database/FPathIndex.h
new file mode 100644
index 0000000..cf92ad1
--- /dev/null
+++ b/Firebase/Database/FPathIndex.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "FIndex.h"
+#import "FPath.h"
+
+@interface FPathIndex : NSObject<FIndex>
+- (id) initWithPath:(FPath *)path;
+@end
diff --git a/Firebase/Database/FPathIndex.m b/Firebase/Database/FPathIndex.m
new file mode 100644
index 0000000..39913aa
--- /dev/null
+++ b/Firebase/Database/FPathIndex.m
@@ -0,0 +1,125 @@
+/*
+ * 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 "FPathIndex.h"
+#import "FUtilities.h"
+#import "FMaxNode.h"
+#import "FEmptyNode.h"
+#import "FSnapshotUtilities.h"
+#import "FNamedNode.h"
+#import "FPath.h"
+
+@interface FPathIndex ()
+ @property (nonatomic, strong) FPath *path;
+@end
+
+@implementation FPathIndex
+
+- (id) initWithPath:(FPath *)path {
+ self = [super init];
+ if (self) {
+ if (path.isEmpty || [path.getFront isEqualToString:@".priority"]) {
+ [NSException raise:NSInvalidArgumentException format:@"Invalid path for PathIndex: %@", path];
+ }
+ _path = path;
+ }
+ return self;
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ id<FNode> child1 = [node1 getChild:self.path];
+ id<FNode> child2 = [node2 getChild:self.path];
+ NSComparisonResult indexCmp = [child1 compare:child2];
+ if (indexCmp == NSOrderedSame) {
+ return [FUtilities compareKey:key1 toKey:key2];
+ } else {
+ return indexCmp;
+ }
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id <FNode>)node {
+ return ![node getChild:self.path].isEmpty;
+}
+
+- (BOOL)indexedValueChangedBetween:(id <FNode>)oldNode and:(id <FNode>)newNode {
+ id<FNode> oldValue = [oldNode getChild:self.path];
+ id<FNode> newValue = [newNode getChild:self.path];
+ return [oldValue compare:newValue] != NSOrderedSame;
+}
+
+- (FNamedNode *)minPost {
+ return FNamedNode.min;
+}
+
+- (FNamedNode *)maxPost {
+ id<FNode> maxNode = [[FEmptyNode emptyNode] updateChild:self.path
+ withNewChild:[FMaxNode maxNode]];
+
+ return [[FNamedNode alloc] initWithName:[FUtilities maxName] andNode:maxNode];
+}
+
+- (FNamedNode*)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ id<FNode> node = [[FEmptyNode emptyNode] updateChild:self.path withNewChild:indexValue];
+ return [[FNamedNode alloc] initWithName:name andNode:node];
+}
+
+- (NSString *)queryDefinition {
+ return [self.path wireFormat];
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"FPathIndex(%@)", self.path];
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // Safe since we're immutable.
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ if (![other isKindOfClass:[FPathIndex class]]) {
+ return NO;
+ }
+ return ([self.path isEqual:((FPathIndex*)other).path]);
+}
+
+- (NSUInteger) hash {
+ return [self.path hash];
+}
+
+@end
diff --git a/Firebase/Database/FPriorityIndex.h b/Firebase/Database/FPriorityIndex.h
new file mode 100644
index 0000000..8b5904d
--- /dev/null
+++ b/Firebase/Database/FPriorityIndex.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "FIndex.h"
+
+@interface FPriorityIndex : NSObject<FIndex>
++ (id<FIndex>) priorityIndex;
+@end
diff --git a/Firebase/Database/FPriorityIndex.m b/Firebase/Database/FPriorityIndex.m
new file mode 100644
index 0000000..2d06ffa
--- /dev/null
+++ b/Firebase/Database/FPriorityIndex.m
@@ -0,0 +1,118 @@
+/*
+ * 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 "FPriorityIndex.h"
+
+#import "FNode.h"
+#import "FUtilities.h"
+#import "FNamedNode.h"
+#import "FEmptyNode.h"
+#import "FLeafNode.h"
+#import "FMaxNode.h"
+
+// TODO: Abstract into some common base class?
+
+@implementation FPriorityIndex
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ id<FNode> child1 = [node1 getPriority];
+ id<FNode> child2 = [node2 getPriority];
+ NSComparisonResult indexCmp = [child1 compare:child2];
+ if (indexCmp == NSOrderedSame) {
+ return [FUtilities compareKey:key1 toKey:key2];
+ } else {
+ return indexCmp;
+ }
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id <FNode>)node {
+ return !node.getPriority.isEmpty;
+}
+
+- (BOOL)indexedValueChangedBetween:(id <FNode>)oldNode and:(id <FNode>)newNode {
+ id<FNode> oldValue = [oldNode getPriority];
+ id<FNode> newValue = [newNode getPriority];
+ return ![oldValue isEqual:newValue];
+}
+
+- (FNamedNode *)minPost {
+ return FNamedNode.min;
+}
+
+- (FNamedNode *)maxPost {
+ return [self makePost:[FMaxNode maxNode] name:[FUtilities maxName]];
+}
+
+- (FNamedNode*)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ id<FNode> node = [[FLeafNode alloc] initWithValue:@"[PRIORITY-POST]" withPriority:indexValue];
+ return [[FNamedNode alloc] initWithName:name andNode:node];
+}
+
+- (NSString *)queryDefinition {
+ return @".priority";
+}
+
+- (NSString *)description {
+ return @"FPriorityIndex";
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // Safe since we're immutable.
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ return [other isKindOfClass:[FPriorityIndex class]];
+}
+
+- (NSUInteger) hash {
+ // chosen by a fair dice roll. Guaranteed to be random
+ return 3155577;
+}
+
++ (id<FIndex>) priorityIndex {
+ static id<FIndex> index;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ index = [[FPriorityIndex alloc] init];
+ });
+
+ return index;
+}
+
+@end
diff --git a/Firebase/Database/FRangedFilter.h b/Firebase/Database/FRangedFilter.h
new file mode 100644
index 0000000..1457778
--- /dev/null
+++ b/Firebase/Database/FRangedFilter.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 "FNodeFilter.h"
+
+@class FQueryParams;
+@class FNamedNode;
+
+@interface FRangedFilter : NSObject<FNodeFilter>
+
+- (id) initWithQueryParams:(FQueryParams *)params;
+- (BOOL) matchesKey:(NSString *)key andNode:(id<FNode>)node;
+
+
+@property (nonatomic, strong, readonly) FNamedNode *startPost;
+@property (nonatomic, strong, readonly) FNamedNode *endPost;
+
+@end
diff --git a/Firebase/Database/FRangedFilter.m b/Firebase/Database/FRangedFilter.m
new file mode 100644
index 0000000..5c4bbeb
--- /dev/null
+++ b/Firebase/Database/FRangedFilter.m
@@ -0,0 +1,118 @@
+/*
+ * 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 "FRangedFilter.h"
+#import "FChildChangeAccumulator.h"
+#import "FNamedNode.h"
+#import "FQueryParams.h"
+#import "FIndexedFilter.h"
+#import "FQueryParams.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FIndexedNode.h"
+
+@interface FRangedFilter ()
+@property (nonatomic, strong, readwrite) id<FNodeFilter> indexedFilter;
+@property (nonatomic, strong, readwrite) id<FIndex> index;
+@property (nonatomic, strong, readwrite) FNamedNode *startPost;
+@property (nonatomic, strong, readwrite) FNamedNode *endPost;
+@end
+
+@implementation FRangedFilter
+- (id) initWithQueryParams:(FQueryParams *)params {
+ self = [super init];
+ if (self) {
+ self.indexedFilter = [[FIndexedFilter alloc] initWithIndex:params.index];
+ self.index = params.index;
+ self.startPost = [FRangedFilter startPostFromQueryParams:params];
+ self.endPost = [FRangedFilter endPostFromQueryParams:params];
+ }
+ return self;
+}
+
+
++ (FNamedNode *) startPostFromQueryParams:(FQueryParams *)params {
+ if ([params hasStart]) {
+ NSString *startKey = params.indexStartKey;
+ return [params.index makePost:params.indexStartValue name:startKey];
+ } else {
+ return params.index.minPost;
+ }
+}
+
++ (FNamedNode *) endPostFromQueryParams:(FQueryParams *)params {
+ if ([params hasEnd]) {
+ NSString *endKey = params.indexEndKey;
+ return [params.index makePost:params.indexEndValue name:endKey];
+ } else {
+ return params.index.maxPost;
+ }
+}
+
+- (BOOL) matchesKey:(NSString *)key andNode:(id<FNode>)node {
+ return ([self.index compareKey:self.startPost.name andNode:self.startPost.node toOtherKey:key andNode:node] <= NSOrderedSame &&
+ [self.index compareKey:key andNode:node toOtherKey:self.endPost.name andNode:self.endPost.node] <= NSOrderedSame);
+}
+
+- (FIndexedNode *)updateChildIn:(FIndexedNode *)oldSnap
+ forChildKey:(NSString *)childKey
+ newChild:(id<FNode>)newChildSnap
+ affectedPath:(FPath *)affectedPath
+ fromSource:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ if (![self matchesKey:childKey andNode:newChildSnap]) {
+ newChildSnap = [FEmptyNode emptyNode];
+ }
+ return [self.indexedFilter updateChildIn:oldSnap
+ forChildKey:childKey
+ newChild:newChildSnap
+ affectedPath:affectedPath
+ fromSource:source
+ accumulator:optChangeAccumulator];
+}
+
+- (FIndexedNode *) updateFullNode:(FIndexedNode *)oldSnap
+ withNewNode:(FIndexedNode *)newSnap
+ accumulator:(FChildChangeAccumulator *)optChangeAccumulator
+{
+ __block FIndexedNode *filtered;
+ if (newSnap.node.isLeafNode) {
+ // Make sure we have a children node with the correct index, not a leaf node
+ filtered = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:self.index];
+ } else {
+ // Dont' support priorities on queries
+ filtered = [newSnap updatePriority:[FEmptyNode emptyNode]];
+ [newSnap.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if (![self matchesKey:key andNode:node]) {
+ filtered = [filtered updateChild:key 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;
+}
+
+@end
diff --git a/Firebase/Database/FTransformedEnumerator.h b/Firebase/Database/FTransformedEnumerator.h
new file mode 100644
index 0000000..75391a8
--- /dev/null
+++ b/Firebase/Database/FTransformedEnumerator.h
@@ -0,0 +1,24 @@
+/*
+ * 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 FTransformedEnumerator : NSEnumerator
+- (id)initWithEnumerator:(NSEnumerator*) enumerator andTransform:(id (^)(id))transform;
+- (id)nextObject;
+
+@end
diff --git a/Firebase/Database/FTransformedEnumerator.m b/Firebase/Database/FTransformedEnumerator.m
new file mode 100644
index 0000000..bb36e94
--- /dev/null
+++ b/Firebase/Database/FTransformedEnumerator.m
@@ -0,0 +1,43 @@
+/*
+ * 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 "FTransformedEnumerator.h"
+
+@interface FTransformedEnumerator ()
+@property (nonatomic, strong) NSEnumerator *enumerator;
+@property (nonatomic, copy) id (^transform)(id);
+@end
+
+@implementation FTransformedEnumerator
+- (id)initWithEnumerator:(NSEnumerator *)enumerator andTransform:(id (^)(id))transform {
+ self = [super init];
+ if (self) {
+ self.enumerator = enumerator;
+ self.transform = transform;
+ }
+ return self;
+}
+
+- (id)nextObject {
+ id next = self.enumerator.nextObject;
+ if (next != nil) {
+ return self.transform(next);
+ } else {
+ return nil;
+ }
+}
+
+@end
diff --git a/Firebase/Database/FTreeSortedDictionary.h b/Firebase/Database/FTreeSortedDictionary.h
new file mode 100644
index 0000000..de75988
--- /dev/null
+++ b/Firebase/Database/FTreeSortedDictionary.h
@@ -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.
+ */
+
+/**
+ * @fileoverview Implementation of an immutable SortedMap using a Left-leaning
+ * Red-Black Tree, adapted from the implementation in Mugs
+ * (http://mads379.github.com/mugs/) by Mads Hartmann Jensen
+ * (mads379@gmail.com).
+ *
+ * Original paper on Left-leaning Red-Black Trees:
+ * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
+ *
+ * Invariant 1: No red node has a red child
+ * Invariant 2: Every leaf path has the same number of black nodes
+ * Invariant 3: Only the left child can be red (left leaning)
+ */
+
+#import <Foundation/Foundation.h>
+#import "FImmutableSortedDictionary.h"
+#import "FLLRBNode.h"
+
+@interface FTreeSortedDictionary : FImmutableSortedDictionary
+
+@property (nonatomic, copy, readonly) NSComparator comparator;
+@property (nonatomic, strong, readonly) id<FLLRBNode> root;
+
+- (id)initWithComparator:(NSComparator)aComparator;
+
+// Override methods to return subtype
+- (FTreeSortedDictionary *) insertKey:(id)aKey withValue:(id)aValue;
+- (FTreeSortedDictionary *) removeKey:(id)aKey;
+
+@end
diff --git a/Firebase/Database/FTreeSortedDictionary.m b/Firebase/Database/FTreeSortedDictionary.m
new file mode 100644
index 0000000..d3b00f9
--- /dev/null
+++ b/Firebase/Database/FTreeSortedDictionary.m
@@ -0,0 +1,342 @@
+/*
+ * 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 "FTreeSortedDictionary.h"
+#import "FLLRBEmptyNode.h"
+#import "FLLRBValueNode.h"
+#import "FTreeSortedDictionaryEnumerator.h"
+
+typedef void (^fbt_void_nsnumber_int)(NSNumber* color, NSUInteger chunkSize);
+
+@interface FTreeSortedDictionary ()
+
+@property (nonatomic, strong) id<FLLRBNode> root;
+@property (nonatomic, copy, readwrite) NSComparator comparator;
+
+@end
+
+@implementation FTreeSortedDictionary
+
+- (id)initWithComparator:(NSComparator)aComparator {
+ self = [super init];
+ if (self) {
+ self.root = [FLLRBEmptyNode emptyNode];
+ self.comparator = aComparator;
+ }
+ return self;
+}
+
+- (id)initWithComparator:(NSComparator)aComparator withRoot:(__unsafe_unretained id<FLLRBNode>)aRoot {
+ self = [super init];
+ if (self) {
+ self.root = aRoot;
+ self.comparator = aComparator;
+ }
+ return self;
+}
+
+/**
+ * Returns a copy of the map, with the specified key/value added or replaced.
+ */
+- (FTreeSortedDictionary *) insertKey:(__unsafe_unretained id)aKey withValue:(__unsafe_unretained id)aValue {
+ return [[FTreeSortedDictionary alloc] initWithComparator:self.comparator
+ withRoot:[[self.root insertKey:aKey forValue:aValue withComparator:self.comparator]
+ copyWith:nil
+ withValue:nil
+ withColor:BLACK
+ withLeft:nil
+ withRight:nil]];
+}
+
+
+- (FTreeSortedDictionary *) removeKey:(__unsafe_unretained id)aKey {
+ // Remove is somewhat expensive even if the key doesn't exist (the tree does rebalancing and stuff). So avoid it.
+ if (![self contains:aKey]) {
+ return self;
+ } else {
+ return [[FTreeSortedDictionary alloc]
+ initWithComparator:self.comparator
+ withRoot:[[self.root remove:aKey withComparator:self.comparator]
+ copyWith:nil
+ withValue:nil
+ withColor:BLACK
+ withLeft:nil
+ withRight:nil]];
+ }
+}
+
+- (id) get:(__unsafe_unretained id) key {
+ if (key == nil) {
+ return nil;
+ }
+ NSComparisonResult cmp;
+ id<FLLRBNode> node = self.root;
+ while(![node isEmpty]) {
+ cmp = self.comparator(key, node.key);
+ if(cmp == NSOrderedSame) {
+ return node.value;
+ }
+ else if (cmp == NSOrderedAscending) {
+ node = node.left;
+ }
+ else {
+ node = node.right;
+ }
+ }
+ return nil;
+}
+
+- (id) getPredecessorKey:(__unsafe_unretained id) key {
+ NSComparisonResult cmp;
+ id<FLLRBNode> node = self.root;
+ id<FLLRBNode> rightParent = nil;
+ while(![node isEmpty]) {
+ cmp = self.comparator(key, node.key);
+ if(cmp == NSOrderedSame) {
+ if(![node.left isEmpty]) {
+ node = node.left;
+ while(! [node.right isEmpty]) {
+ node = node.right;
+ }
+ return node.key;
+ }
+ else if (rightParent != nil) {
+ return rightParent.key;
+ }
+ else {
+ return nil;
+ }
+ }
+ else if (cmp == NSOrderedAscending) {
+ node = node.left;
+ }
+ else if (cmp == NSOrderedDescending) {
+ rightParent = node;
+ node = node.right;
+ }
+ }
+ @throw [NSException exceptionWithName:@"NonexistentKey" reason:@"getPredecessorKey called with nonexistent key." userInfo:@{@"key": [key description] }];
+}
+
+- (BOOL) isEmpty {
+ return [self.root isEmpty];
+}
+
+- (int) count {
+ return [self.root count];
+}
+
+- (id) minKey {
+ return [self.root minKey];
+}
+
+- (id) maxKey {
+ return [self.root maxKey];
+}
+
+- (void) enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block
+{
+ [self enumerateKeysAndObjectsReverse:NO usingBlock:block];
+}
+
+- (void) enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block
+{
+ if (reverse) {
+ __block BOOL stop = NO;
+ [self.root reverseTraversal:^BOOL(id key, id value) {
+ block(key, value, &stop);
+ return stop;
+ }];
+ } else {
+ __block BOOL stop = NO;
+ [self.root inorderTraversal:^BOOL(id key, id value) {
+ block(key, value, &stop);
+ return stop;
+ }];
+ }
+}
+
+- (BOOL) contains:(__unsafe_unretained id)key {
+ return ([self objectForKey:key] != nil);
+}
+
+- (NSEnumerator *) keyEnumerator {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:nil isReverse:NO];
+}
+
+- (NSEnumerator *) keyEnumeratorFrom:(id)startKey {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:startKey isReverse:NO];
+}
+
+- (NSEnumerator *) reverseKeyEnumerator {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:nil isReverse:YES];
+}
+
+- (NSEnumerator *) reverseKeyEnumeratorFrom:(id)startKey {
+ return [[FTreeSortedDictionaryEnumerator alloc]
+ initWithImmutableSortedDictionary:self startKey:startKey isReverse:YES];
+}
+
+
+#pragma mark -
+#pragma mark Tree Builder
+
+// Code to efficiently build a RB Tree
+typedef struct _base1_2list {
+ unsigned int bits;
+ unsigned short count;
+ unsigned short current;
+} Base1_2List;
+
+Base1_2List *base1_2List_new(unsigned int length);
+void base1_2List_free(Base1_2List* list);
+unsigned int log_base2(unsigned int num);
+BOOL base1_2List_next(Base1_2List* list);
+
+unsigned int log_base2(unsigned int num) {
+ return (unsigned int)(log(num) / log(2));
+}
+
+/**
+ * Works like an iterator, so it moves to the next bit. Do not call more than list->count times.
+ * @return whether or not the next bit is a 1 in base {1,2}.
+ */
+BOOL base1_2List_next(Base1_2List* list) {
+ BOOL result = !(list->bits & (0x1 << list->current));
+ list->current--;
+ return result;
+}
+
+static inline unsigned bit_mask(int x) {
+ return (x >= sizeof(unsigned) * CHAR_BIT) ? (unsigned) -1 : (1U << x) - 1;
+}
+
+/**
+ * We represent the base{1,2} number as the combination of a binary number and a number of bits that we care about
+ * We iterate backwards, from most significant bit to least, to build up the llrb nodes. 0 base 2 => 1 base {1,2}, 1 base 2 => 2 base {1,2}
+ */
+Base1_2List *base1_2List_new(unsigned int length) {
+ size_t sz = sizeof(Base1_2List);
+ Base1_2List* list = calloc(1, sz);
+ // Calculate the number of bits that we care about
+ list->count = (unsigned short)log_base2(length + 1);
+ unsigned int mask = bit_mask(list->count);
+ list->bits = (length + 1) & mask;
+ list->current = list->count - 1;
+ return list;
+}
+
+
+void base1_2List_free(Base1_2List* list) {
+ free(list);
+}
+
++ (id<FLLRBNode>) buildBalancedTree:(NSArray *)keys dictionary:(NSDictionary *)dictionary subArrayStartIndex:(NSUInteger)startIndex length:(NSUInteger)length {
+ length = MIN(keys.count - startIndex, length); // Bound length by the actual length of the array
+ if (length == 0) {
+ return nil;
+ } else if (length == 1) {
+ id key = keys[startIndex];
+ return [[FLLRBValueNode alloc] initWithKey:key withValue:dictionary[key] withColor:BLACK withLeft:nil withRight:nil];
+ } else {
+ NSUInteger middle = length / 2;
+ id<FLLRBNode> left = [FTreeSortedDictionary buildBalancedTree:keys dictionary:dictionary subArrayStartIndex:startIndex length:middle];
+ id<FLLRBNode> right = [FTreeSortedDictionary buildBalancedTree:keys dictionary:dictionary subArrayStartIndex:(startIndex+middle+1) length:middle];
+ id key = keys[startIndex + middle];
+ return [[FLLRBValueNode alloc] initWithKey:key withValue:dictionary[key] withColor:BLACK withLeft:left withRight:right];
+ }
+}
+
++ (id<FLLRBNode>) rootFrom12List:(Base1_2List *)base1_2List keyList:(NSArray *)keyList dictionary:(NSDictionary *)dictionary {
+ __block id<FLLRBNode> root = nil;
+ __block id<FLLRBNode> node = nil;
+ __block NSUInteger index = keyList.count;
+
+ fbt_void_nsnumber_int buildPennant = ^(NSNumber* color, NSUInteger chunkSize) {
+ NSUInteger startIndex = index - chunkSize + 1;
+ index -= chunkSize;
+ id key = keyList[index];
+ id<FLLRBNode> childTree = [self buildBalancedTree:keyList dictionary:dictionary subArrayStartIndex:startIndex length:(chunkSize - 1)];
+ id<FLLRBNode> pennant = [[FLLRBValueNode alloc] initWithKey:key withValue:dictionary[key] withColor:color withLeft:nil withRight:childTree];
+ //attachPennant(pennant);
+ if (node) {
+ node.left = pennant;
+ node = pennant;
+ } else {
+ root = pennant;
+ node = pennant;
+ }
+ };
+
+ for (int i = 0; i < base1_2List->count; ++i) {
+ BOOL isOne = base1_2List_next(base1_2List);
+ NSUInteger chunkSize = (NSUInteger)pow(2.0, base1_2List->count - (i + 1));
+ if (isOne) {
+ buildPennant(BLACK, chunkSize);
+ } else {
+ buildPennant(BLACK, chunkSize);
+ buildPennant(RED, chunkSize);
+ }
+ }
+ return root;
+}
+
+/**
+ * Uses the algorithm linked here:
+ * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.1458
+ */
+
++ (FImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary withComparator:(NSComparator)comparator
+{
+ // Steps:
+ // 0. Sort the array
+ // 1. Calculate the 1-2 number
+ // 2. Build From 1-2 number
+ // 0. for each digit in 1-2 number
+ // 0. calculate chunk size
+ // 1. build 1 or 2 pennants of that size
+ // 2. attach pennants and update node pointer
+ // 1. return root
+ NSMutableArray *sortedKeyList = [NSMutableArray arrayWithCapacity:dictionary.count];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ [sortedKeyList addObject:key];
+ }];
+ [sortedKeyList sortUsingComparator:comparator];
+
+ [sortedKeyList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+ if (idx > 0) {
+ if (comparator(sortedKeyList[idx - 1], obj) != NSOrderedAscending) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create FImmutableSortedDictionary with keys with same ordering!"];
+ }
+ }
+ }];
+
+ Base1_2List* list = base1_2List_new((unsigned int)sortedKeyList.count);
+ id<FLLRBNode> root = [self rootFrom12List:list keyList:sortedKeyList dictionary:dictionary];
+ base1_2List_free(list);
+
+ if (root != nil) {
+ return [[FTreeSortedDictionary alloc] initWithComparator:comparator withRoot:root];
+ } else {
+ return [[FTreeSortedDictionary alloc] initWithComparator:comparator];
+ }
+}
+
+@end
+
diff --git a/Firebase/Database/FValueIndex.h b/Firebase/Database/FValueIndex.h
new file mode 100644
index 0000000..0f1c7f7
--- /dev/null
+++ b/Firebase/Database/FValueIndex.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "FIndex.h"
+
+
+@interface FValueIndex : NSObject<FIndex>
++ (id<FIndex>) valueIndex;
+@end
diff --git a/Firebase/Database/FValueIndex.m b/Firebase/Database/FValueIndex.m
new file mode 100644
index 0000000..7ef9bff
--- /dev/null
+++ b/Firebase/Database/FValueIndex.m
@@ -0,0 +1,106 @@
+/*
+ * 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 "FValueIndex.h"
+#import "FNamedNode.h"
+#import "FSnapshotUtilities.h"
+#import "FUtilities.h"
+#import "FMaxNode.h"
+
+@implementation FValueIndex
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+{
+ NSComparisonResult indexCmp = [node1 compare:node2];
+ if (indexCmp == NSOrderedSame) {
+ return [FUtilities compareKey:key1 toKey:key2];
+ } else {
+ return indexCmp;
+ }
+}
+
+- (NSComparisonResult) compareKey:(NSString *)key1
+ andNode:(id<FNode>)node1
+ toOtherKey:(NSString *)key2
+ andNode:(id<FNode>)node2
+ reverse:(BOOL)reverse
+{
+ if (reverse) {
+ return [self compareKey:key2 andNode:node2 toOtherKey:key1 andNode:node1];
+ } else {
+ return [self compareKey:key1 andNode:node1 toOtherKey:key2 andNode:node2];
+ }
+}
+
+- (NSComparisonResult) compareNamedNode:(FNamedNode *)namedNode1 toNamedNode:(FNamedNode *)namedNode2
+{
+ return [self compareKey:namedNode1.name andNode:namedNode1.node toOtherKey:namedNode2.name andNode:namedNode2.node];
+}
+
+- (BOOL)isDefinedOn:(id<FNode>)node {
+ return YES;
+}
+
+- (BOOL)indexedValueChangedBetween:(id<FNode>)oldNode and:(id<FNode>)newNode {
+ return ![oldNode isEqual:newNode];
+}
+
+- (FNamedNode *)minPost {
+ return FNamedNode.min;
+}
+
+- (FNamedNode *)maxPost {
+ return FNamedNode.max;
+}
+
+- (FNamedNode *)makePost:(id<FNode>)indexValue name:(NSString*)name {
+ return [[FNamedNode alloc] initWithName:name andNode:indexValue];
+}
+
+- (NSString *)queryDefinition {
+ return @".value";
+}
+
+- (NSString *) description {
+ return @"FValueIndex";
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ return self;
+}
+
+- (BOOL) isEqual:(id)other {
+ // since we're a singleton.
+ return (other == self);
+}
+
+- (NSUInteger) hash {
+ return [@".value" hash];
+}
+
+
++ (id<FIndex>) valueIndex {
+ static id<FIndex> valueIndex;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ valueIndex = [[FValueIndex alloc] init];
+ });
+ return valueIndex;
+}
+@end
diff --git a/Firebase/Database/FViewProcessor.h b/Firebase/Database/FViewProcessor.h
new file mode 100644
index 0000000..59bfd2d
--- /dev/null
+++ b/Firebase/Database/FViewProcessor.h
@@ -0,0 +1,41 @@
+/*
+ * 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 FViewCache;
+@class FViewProcessorResult;
+@class FChildChangeAccumulator;
+@protocol FNode;
+@class FWriteTreeRef;
+@class FPath;
+@protocol FOperation;
+@protocol FNodeFilter;
+
+
+@interface FViewProcessor : NSObject
+
+- (id)initWithFilter:(id<FNodeFilter>)nodeFilter;
+
+- (FViewProcessorResult *)applyOperationOn:(FViewCache *)oldViewCache operation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache completeCache:(id <FNode>)optCompleteCache;
+- (FViewCache *) revertUserWriteOn:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator;
+
+
+@end
diff --git a/Firebase/Database/FViewProcessor.m b/Firebase/Database/FViewProcessor.m
new file mode 100644
index 0000000..41ff91d
--- /dev/null
+++ b/Firebase/Database/FViewProcessor.m
@@ -0,0 +1,654 @@
+/*
+ * 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 "FViewProcessor.h"
+#import "FCompleteChildSource.h"
+#import "FWriteTreeRef.h"
+#import "FViewCache.h"
+#import "FCacheNode.h"
+#import "FNode.h"
+#import "FOperation.h"
+#import "FOperationSource.h"
+#import "FChildChangeAccumulator.h"
+#import "FNodeFilter.h"
+#import "FOverwrite.h"
+#import "FMerge.h"
+#import "FAckUserWrite.h"
+#import "FViewProcessorResult.h"
+#import "FIRDataEventType.h"
+#import "FChange.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FPath.h"
+#import "FKeyIndex.h"
+#import "FCompoundWrite.h"
+#import "FImmutableTree.h"
+
+/**
+* An implementation of FCompleteChildSource that never returns any additional children
+*/
+@interface FNoCompleteChildSource: NSObject<FCompleteChildSource>
+@end
+
+@implementation FNoCompleteChildSource
++ (FNoCompleteChildSource *) instance {
+ static FNoCompleteChildSource *source = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ source = [[FNoCompleteChildSource alloc] init];
+ });
+ return source;
+}
+
+- (id<FNode>) completeChild:(NSString *)childKey {
+ return nil;
+}
+
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse {
+ return nil;
+}
+@end
+
+/**
+* An implementation of FCompleteChildSource that uses a FWriteTree in addition to any other server data or
+* old event caches available to calculate complete children.
+*/
+@interface FWriteTreeCompleteChildSource: NSObject<FCompleteChildSource>
+@property (nonatomic, strong) FWriteTreeRef *writes;
+@property (nonatomic, strong) FViewCache *viewCache;
+@property (nonatomic, strong) id<FNode> optCompleteServerCache;
+@end
+
+@implementation FWriteTreeCompleteChildSource
+- (id) initWithWrites:(FWriteTreeRef *)writes viewCache:(FViewCache *)viewCache serverCache:(id<FNode>)optCompleteServerCache {
+ self = [super init];
+ if (self) {
+ self.writes = writes;
+ self.viewCache = viewCache;
+ self.optCompleteServerCache = optCompleteServerCache;
+ }
+ return self;
+}
+
+- (id<FNode>) completeChild:(NSString *)childKey {
+ FCacheNode *node = self.viewCache.cachedEventSnap;
+ if ([node isCompleteForChild:childKey]) {
+ return [node.node getImmediateChild:childKey];
+ } else {
+ FCacheNode *serverNode;
+ if (self.optCompleteServerCache) {
+ // Since we're only ever getting child nodes, we can use the key index here
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:self.optCompleteServerCache index:[FKeyIndex keyIndex]];
+ serverNode = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:YES isFiltered:NO];
+ } else {
+ serverNode = self.viewCache.cachedServerSnap;
+ }
+ return [self.writes calculateCompleteChild:childKey cache:serverNode];
+ }
+}
+
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse {
+ id<FNode> completeServerData = self.optCompleteServerCache != nil
+ ? self.optCompleteServerCache
+ : self.viewCache.completeServerSnap;
+ return [self.writes calculateNextNodeAfterPost:child
+ completeServerData:completeServerData
+ reverse:reverse
+ index:index];
+}
+
+@end
+
+@interface FViewProcessor ()
+@property (nonatomic, strong) id<FNodeFilter> filter;
+@end
+
+@implementation FViewProcessor
+
+- (id)initWithFilter:(id<FNodeFilter>)nodeFilter {
+ self = [super init];
+ if (self) {
+ self.filter = nodeFilter;
+ }
+ return self;
+}
+
+- (FViewProcessorResult *)applyOperationOn:(FViewCache *)oldViewCache operation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache completeCache:(id <FNode>)optCompleteCache {
+ FChildChangeAccumulator *accumulator = [[FChildChangeAccumulator alloc] init];
+ FViewCache *newViewCache;
+
+ if (operation.type == FOperationTypeOverwrite) {
+ FOverwrite *overwrite = (FOverwrite *) operation;
+ if (operation.source.fromUser) {
+ newViewCache = [self applyUserOverwriteTo:oldViewCache
+ changePath:overwrite.path
+ changedSnap:overwrite.snap
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ NSAssert(operation.source.fromServer, @"Unknown source for overwrite.");
+ // We filter the node if it's a tagged update or the node has been previously filtered and the update is
+ // not at the root in which case it is ok (and necessary) to mark the node unfiltered again
+ BOOL filterServerNode = overwrite.source.isTagged || (oldViewCache.cachedServerSnap.isFiltered &&
+ !overwrite.path.isEmpty);
+ newViewCache = [self applyServerOverwriteTo:oldViewCache
+ changePath:overwrite.path
+ snap:overwrite.snap
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeMerge) {
+ FMerge *merge = (FMerge*)operation;
+ if (operation.source.fromUser) {
+ newViewCache = [self applyUserMergeTo:oldViewCache
+ path:merge.path
+ changedChildren:merge.children
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ NSAssert(operation.source.fromServer, @"Unknown source for merge.");
+ // We filter the node if it's a tagged update or the node has been previously filtered
+ BOOL filterServerNode = merge.source.isTagged || oldViewCache.cachedServerSnap.isFiltered;
+ newViewCache = [self applyServerMergeTo:oldViewCache
+ path:merge.path
+ changedChildren:merge.children
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeAckUserWrite) {
+ FAckUserWrite *ackWrite = (FAckUserWrite *) operation;
+ if (!ackWrite.revert) {
+ newViewCache = [self ackUserWriteOn:oldViewCache
+ ackPath:ackWrite.path
+ affectedTree:ackWrite.affectedTree
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ newViewCache = [self revertUserWriteOn:oldViewCache
+ path:ackWrite.path
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeListenComplete) {
+ newViewCache = [self listenCompleteOldCache:oldViewCache
+ path:operation.path
+ writesCache:writesCache
+ serverCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Unknown operation encountered %zd.", operation.type];
+ return nil;
+ }
+
+ NSArray *changes = [self maybeAddValueFromOldViewCache:oldViewCache newViewCache:newViewCache changes:accumulator.changes];
+ FViewProcessorResult *results = [[FViewProcessorResult alloc] initWithViewCache:newViewCache changes:changes];
+ return results;
+}
+
+- (NSArray *) maybeAddValueFromOldViewCache:(FViewCache *)oldViewCache newViewCache:(FViewCache *)newViewCache changes:(NSArray *)changes {
+ NSArray *newChanges = changes;
+ FCacheNode *eventSnap = newViewCache.cachedEventSnap;
+ if (eventSnap.isFullyInitialized) {
+ BOOL isLeafOrEmpty = eventSnap.node.isLeafNode || eventSnap.node.isEmpty;
+ if ([changes count] > 0 ||
+ !oldViewCache.cachedEventSnap.isFullyInitialized ||
+ (isLeafOrEmpty && ![eventSnap.node isEqual:oldViewCache.completeEventSnap]) ||
+ ![eventSnap.node.getPriority isEqual:oldViewCache.completeEventSnap.getPriority]) {
+ FChange *valueChange = [[FChange alloc] initWithType:FIRDataEventTypeValue indexedNode:eventSnap.indexedNode];
+ NSMutableArray *mutableChanges = [changes mutableCopy];
+ [mutableChanges addObject:valueChange];
+ newChanges = mutableChanges;
+ }
+ }
+ return newChanges;
+}
+
+- (FViewCache *) generateEventCacheAfterServerEvent:(FViewCache *)viewCache
+ path:(FPath *)changePath
+ writesCache:(FWriteTreeRef *)writesCache
+ source:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldEventSnap = viewCache.cachedEventSnap;
+ if ([writesCache shadowingWriteAtPath:changePath] != nil) {
+ // we have a shadowing write, ignore changes.
+ return viewCache;
+ } else {
+ FIndexedNode *newEventCache;
+ if (changePath.isEmpty) {
+ // TODO: figure out how this plays with "sliding ack windows"
+ NSAssert(viewCache.cachedServerSnap.isFullyInitialized, @"If change path is empty, we must have complete server data");
+ id<FNode> nodeWithLocalWrites;
+ if (viewCache.cachedServerSnap.isFiltered) {
+ // We need to special case this, because we need to only apply writes to complete children, or
+ // we might end up raising events for incomplete children. If the server data is filtered deep
+ // writes cannot be guaranteed to be complete
+ id<FNode> serverCache = viewCache.completeServerSnap;
+ FChildrenNode *completeChildren = ([serverCache isKindOfClass:[FChildrenNode class]]) ? serverCache : [FEmptyNode emptyNode];
+ nodeWithLocalWrites = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:completeChildren];
+ } else {
+ nodeWithLocalWrites = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ }
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:nodeWithLocalWrites index:self.filter.index];
+ newEventCache = [self.filter updateFullNode:viewCache.cachedEventSnap.indexedNode
+ withNewNode:indexedNode
+ accumulator:accumulator];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if ([childKey isEqualToString:@".priority"]) {
+ NSAssert(changePath.length == 1, @"Can't have a priority with additional path components");
+ id<FNode> oldEventNode = oldEventSnap.node;
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+ // we might have overwrites for this priority
+ id<FNode> updatedPriority = [writesCache calculateEventCacheAfterServerOverwriteWithChildPath:changePath
+ existingEventSnap:oldEventNode
+ existingServerSnap:serverNode];
+ if (updatedPriority != nil) {
+ newEventCache = [self.filter updatePriority:updatedPriority forNode:oldEventSnap.indexedNode];
+ } else {
+ // priority didn't change, keep old node
+ newEventCache = oldEventSnap.indexedNode;
+ }
+ } else {
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> newEventChild;
+ if ([oldEventSnap isCompleteForChild:childKey]) {
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+ id<FNode> eventChildUpdate = [writesCache calculateEventCacheAfterServerOverwriteWithChildPath:changePath existingEventSnap:oldEventSnap.node existingServerSnap:serverNode];
+ if (eventChildUpdate != nil) {
+ newEventChild = [[oldEventSnap.node getImmediateChild:childKey] updateChild:childChangePath withNewChild:eventChildUpdate];
+ } else {
+ // Nothing changed, just keep the old child
+ newEventChild = [oldEventSnap.node getImmediateChild:childKey];
+ }
+ } else {
+ newEventChild = [writesCache calculateCompleteChild:childKey cache:viewCache.cachedServerSnap];
+ }
+ if (newEventChild != nil) {
+ newEventCache = [self.filter updateChildIn:oldEventSnap.indexedNode
+ forChildKey:childKey
+ newChild:newEventChild
+ affectedPath:childChangePath
+ fromSource:source
+ accumulator:accumulator];
+ } else {
+ // No complete children available or no change
+ newEventCache = oldEventSnap.indexedNode;
+ }
+ }
+ }
+ return [viewCache updateEventSnap:newEventCache
+ isComplete:(oldEventSnap.isFullyInitialized || changePath.isEmpty)
+ isFiltered:self.filter.filtersNodes];
+ }
+}
+
+- (FViewCache *) applyServerOverwriteTo:(FViewCache *)oldViewCache changePath:(FPath *)changePath snap:(id<FNode>)changedSnap
+ writesCache:(FWriteTreeRef *)writesCache completeCache:(id<FNode>)optCompleteCache
+ filterServerNode:(BOOL)filterServerNode accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldServerSnap = oldViewCache.cachedServerSnap;
+ FIndexedNode *newServerCache;
+ id<FNodeFilter> serverFilter = filterServerNode ? self.filter : self.filter.indexedFilter;
+
+ if (changePath.isEmpty) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:changedSnap index:serverFilter.index];
+ newServerCache = [serverFilter updateFullNode:oldServerSnap.indexedNode withNewNode:indexed accumulator:nil];
+ } else if (serverFilter.filtersNodes && !oldServerSnap.isFiltered) {
+ // We want to filter the server node, but we didn't filter the server node yet, so simulate a full update
+ NSAssert(![changePath isEmpty], @"An empty path should been caught in the other branch");
+ NSString *childKey = [changePath getFront];
+ FPath *updatePath = [changePath popFront];
+ id<FNode> newChild = [[oldServerSnap.node getImmediateChild:childKey] updateChild:updatePath
+ withNewChild:changedSnap];
+ FIndexedNode *indexed = [oldServerSnap.indexedNode updateChild:childKey withNewChild:newChild];
+ newServerCache = [serverFilter updateFullNode:oldServerSnap.indexedNode withNewNode:indexed accumulator:nil];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if (![oldServerSnap isCompleteForPath:changePath] && changePath.length > 1) {
+ // We don't update incomplete nodes with updates intended for other listeners.
+ return oldViewCache;
+ }
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> childNode = [oldServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChildNode = [childNode updateChild:childChangePath withNewChild:changedSnap];
+ if ([childKey isEqualToString:@".priority"]) {
+ newServerCache = [serverFilter updatePriority:newChildNode forNode:oldServerSnap.indexedNode];
+ } else {
+ newServerCache = [serverFilter updateChildIn:oldServerSnap.indexedNode
+ forChildKey:childKey
+ newChild:newChildNode
+ affectedPath:childChangePath
+ fromSource:[FNoCompleteChildSource instance]
+ accumulator:nil];
+ }
+ }
+ FViewCache *newViewCache = [oldViewCache updateServerSnap:newServerCache
+ isComplete:(oldServerSnap.isFullyInitialized || changePath.isEmpty)
+ isFiltered:serverFilter.filtersNodes];
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:newViewCache
+ serverCache:optCompleteCache];
+ return [self generateEventCacheAfterServerEvent:newViewCache
+ path:changePath
+ writesCache:writesCache
+ source:source
+ accumulator:accumulator];
+}
+
+- (FViewCache *) applyUserOverwriteTo:(FViewCache *)oldViewCache
+ changePath:(FPath *)changePath
+ changedSnap:(id<FNode>)changedSnap
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldEventSnap = oldViewCache.cachedEventSnap;
+ FViewCache *newViewCache;
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:oldViewCache
+ serverCache:optCompleteCache];
+ if (changePath.isEmpty) {
+ FIndexedNode *newIndexed = [FIndexedNode indexedNodeWithNode:changedSnap index:self.filter.index];
+ FIndexedNode *newEventCache = [self.filter updateFullNode:oldEventSnap.indexedNode
+ withNewNode:newIndexed
+ accumulator:accumulator];
+ newViewCache = [oldViewCache updateEventSnap:newEventCache isComplete:YES isFiltered:self.filter.filtersNodes];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if ([childKey isEqualToString:@".priority"]) {
+ FIndexedNode *newEventCache = [self.filter updatePriority:changedSnap
+ forNode:oldViewCache.cachedEventSnap.indexedNode];
+ newViewCache = [oldViewCache updateEventSnap:newEventCache
+ isComplete:oldEventSnap.isFullyInitialized
+ isFiltered:oldEventSnap.isFiltered];
+ } else {
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> oldChild = [oldEventSnap.node getImmediateChild:childKey];
+ id<FNode> newChild;
+ if (childChangePath.isEmpty) {
+ // Child overwrite, we can replace the child
+ newChild = changedSnap;
+ } else {
+ id<FNode> childNode = [source completeChild:childKey];
+ if (childNode != nil) {
+ if ([[childChangePath getBack] isEqualToString:@".priority"] && [childNode getChild:[childChangePath parent]].isEmpty) {
+ // This is a priority update on an empty node. If this node exists on the server, the server
+ // will send down the priority in the update, so ignore for now
+ newChild = childNode;
+ } else {
+ newChild = [childNode updateChild:childChangePath withNewChild:changedSnap];
+ }
+ } else {
+ newChild = [FEmptyNode emptyNode];
+ }
+ }
+ if (![oldChild isEqual:newChild]) {
+ FIndexedNode *newEventSnap = [self.filter updateChildIn:oldEventSnap.indexedNode
+ forChildKey:childKey
+ newChild:newChild
+ affectedPath:childChangePath
+ fromSource:source
+ accumulator:accumulator];
+ newViewCache = [oldViewCache updateEventSnap:newEventSnap isComplete:oldEventSnap.isFullyInitialized isFiltered:self.filter.filtersNodes];
+ } else {
+ newViewCache = oldViewCache;
+ }
+ }
+ }
+ return newViewCache;
+}
+
++ (BOOL) cache:(FViewCache *)viewCache hasChild:(NSString *)childKey {
+ return [viewCache.cachedEventSnap isCompleteForChild:childKey];
+}
+
+/**
+* @param changedChildren NSDictionary of child name (NSString*) to child value (id<FNode>)
+*/
+- (FViewCache *) applyUserMergeTo:(FViewCache *)viewCache
+ path:(FPath *)path
+ changedChildren:(FCompoundWrite *)changedChildren
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)serverCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ // HACK: In the case of a limit query, there may be some changes that bump things out of the
+ // window leaving room for new items. It's important we process these changes first, so we
+ // iterate the changes twice, first processing any that affect items currently in view.
+ // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
+ // and event snap. I'm not sure if this will result in edge cases when a child is in one but
+ // not the other.
+ __block FViewCache *curViewCache = viewCache;
+
+ [changedChildren enumerateWrites:^(FPath *relativePath, id<FNode> childNode, BOOL *stop) {
+ FPath *writePath = [path child:relativePath];
+ if ([FViewProcessor cache:viewCache hasChild:[writePath getFront]]) {
+ curViewCache = [self applyUserOverwriteTo:curViewCache
+ changePath:writePath
+ changedSnap:childNode
+ writesCache:writesCache
+ completeCache:serverCache
+ accumulator:accumulator];
+ }
+ }];
+
+ [changedChildren enumerateWrites:^(FPath *relativePath, id<FNode> childNode, BOOL *stop) {
+ FPath *writePath = [path child:relativePath];
+ if (![FViewProcessor cache:viewCache hasChild:[writePath getFront]]) {
+ curViewCache = [self applyUserOverwriteTo:curViewCache
+ changePath:writePath
+ changedSnap:childNode
+ writesCache:writesCache
+ completeCache:serverCache
+ accumulator:accumulator];
+ }
+ }];
+
+ return curViewCache;
+}
+
+- (FViewCache *) applyServerMergeTo:(FViewCache *)viewCache
+ path:(FPath *)path
+ changedChildren:(FCompoundWrite *)changedChildren
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)serverCache
+ filterServerNode:(BOOL)filterServerNode
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ // If we don't have a cache yet, this merge was intended for a previously listen in the same location. Ignore it and
+ // wait for the complete data update coming soon.
+ if (viewCache.cachedServerSnap.node.isEmpty && !viewCache.cachedServerSnap.isFullyInitialized) {
+ return viewCache;
+ }
+
+ // HACK: In the case of a limit query, there may be some changes that bump things out of the
+ // window leaving room for new items. It's important we process these changes first, so we
+ // iterate the changes twice, first processing any that affect items currently in view.
+ // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
+ // and event snap. I'm not sure if this will result in edge cases when a child is in one but
+ // not the other.
+ __block FViewCache *curViewCache = viewCache;
+ FCompoundWrite *actualMerge;
+ if (path.isEmpty) {
+ actualMerge = changedChildren;
+ } else {
+ actualMerge = [[FCompoundWrite emptyWrite] addCompoundWrite:changedChildren atPath:path];
+ }
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+
+ NSDictionary *childCompoundWrites = actualMerge.childCompoundWrites;
+ [childCompoundWrites enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FCompoundWrite *childMerge, BOOL *stop) {
+ if ([serverNode hasChild:childKey]) {
+ id<FNode> serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChild = [childMerge applyToNode:serverChild];
+ curViewCache = [self applyServerOverwriteTo:curViewCache
+ changePath:[[FPath alloc] initWith:childKey]
+ snap:newChild
+ writesCache:writesCache
+ completeCache:serverCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ }];
+
+ [childCompoundWrites enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FCompoundWrite *childMerge, BOOL *stop) {
+ bool isUnknownDeepMerge = ![viewCache.cachedServerSnap isCompleteForChild:childKey] && childMerge.rootWrite == nil;
+ if (![serverNode hasChild:childKey] && !isUnknownDeepMerge) {
+ id<FNode> serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChild = [childMerge applyToNode:serverChild];
+ curViewCache = [self applyServerOverwriteTo:curViewCache
+ changePath:[[FPath alloc] initWith:childKey]
+ snap:newChild
+ writesCache:writesCache
+ completeCache:serverCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ }];
+
+ return curViewCache;
+}
+
+- (FViewCache *) ackUserWriteOn:(FViewCache *)viewCache
+ ackPath:(FPath *)ackPath
+ affectedTree:(FImmutableTree *)affectedTree
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id <FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+
+ if ([writesCache shadowingWriteAtPath:ackPath] != nil) {
+ return viewCache;
+ }
+
+ // Only filter server node if it is currently filtered
+ BOOL filterServerNode = viewCache.cachedServerSnap.isFiltered;
+
+ // Essentially we'll just get our existing server cache for the affected paths and re-apply it as a server update
+ // now that it won't be shadowed.
+ FCacheNode *serverCache = viewCache.cachedServerSnap;
+ if (affectedTree.value != nil) {
+ // This is an overwrite.
+ if ((ackPath.isEmpty && serverCache.isFullyInitialized) || [serverCache isCompleteForPath:ackPath]) {
+ return [self applyServerOverwriteTo:viewCache changePath:ackPath snap:[serverCache.node getChild:ackPath]
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ } else if (ackPath.isEmpty) {
+ // This is a goofy edge case where we are acking data at this location but don't have full data. We
+ // should just re-apply whatever we have in our cache as a merge.
+ FCompoundWrite *changedChildren = [FCompoundWrite emptyWrite];
+ for(FNamedNode *child in serverCache.node.childEnumerator) {
+ changedChildren = [changedChildren addWrite:child.node atKey:child.name];
+ }
+ return [self applyServerMergeTo:viewCache path:ackPath changedChildren:changedChildren
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ } else {
+ return viewCache;
+ }
+ } else {
+ // This is a merge.
+ __block FCompoundWrite *changedChildren = [FCompoundWrite emptyWrite];
+ [affectedTree forEach:^(FPath *mergePath, id value) {
+ FPath *serverCachePath = [ackPath child:mergePath];
+ if ([serverCache isCompleteForPath:serverCachePath]) {
+ changedChildren = [changedChildren addWrite:[serverCache.node getChild:serverCachePath] atPath:mergePath];
+ }
+ }];
+ return [self applyServerMergeTo:viewCache path:ackPath changedChildren:changedChildren
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ }
+}
+
+- (FViewCache *) revertUserWriteOn:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ if ([writesCache shadowingWriteAtPath:path] != nil) {
+ return viewCache;
+ } else {
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:viewCache
+ serverCache:optCompleteCache];
+ FIndexedNode *oldEventCache = viewCache.cachedEventSnap.indexedNode;
+ FIndexedNode *newEventCache;
+ if (path.isEmpty || [[path getFront] isEqualToString:@".priority"]) {
+ id<FNode> newNode;
+ if (viewCache.cachedServerSnap.isFullyInitialized) {
+ newNode = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ } else {
+ newNode = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:viewCache.cachedServerSnap.node];
+ }
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:newNode index:self.filter.index];
+ newEventCache = [self.filter updateFullNode:oldEventCache withNewNode:indexedNode accumulator:accumulator];
+ } else {
+ NSString *childKey = [path getFront];
+ id<FNode> newChild = [writesCache calculateCompleteChild:childKey cache:viewCache.cachedServerSnap];
+ if (newChild == nil && [viewCache.cachedServerSnap isCompleteForChild:childKey]) {
+ newChild = [oldEventCache.node getImmediateChild:childKey];
+ }
+ if (newChild != nil) {
+ newEventCache = [self.filter updateChildIn:oldEventCache
+ forChildKey:childKey
+ newChild:newChild
+ affectedPath:[path popFront]
+ fromSource:source
+ accumulator:accumulator];
+ } else if (newChild == nil && [viewCache.cachedEventSnap.node hasChild:childKey]) {
+ // No complete child available, delete the existing one, if any
+ newEventCache = [self.filter updateChildIn:oldEventCache
+ forChildKey:childKey
+ newChild:[FEmptyNode emptyNode]
+ affectedPath:[path popFront]
+ fromSource:source
+ accumulator:accumulator];
+ } else {
+ newEventCache = oldEventCache;
+ }
+ if (newEventCache.node.isEmpty && viewCache.cachedServerSnap.isFullyInitialized) {
+ // We might have reverted all child writes. Maybe the old event was a leaf node.
+ id<FNode> complete = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ if (complete.isLeafNode) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:complete];
+ newEventCache = [self.filter updateFullNode:newEventCache
+ withNewNode:indexed
+ accumulator:accumulator];
+ }
+ }
+ }
+ BOOL complete = viewCache.cachedServerSnap.isFullyInitialized || [writesCache shadowingWriteAtPath:[FPath empty]] != nil;
+ return [viewCache updateEventSnap:newEventCache isComplete:complete isFiltered:self.filter.filtersNodes];
+ }
+}
+
+- (FViewCache *) listenCompleteOldCache:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(id<FNode>)servercache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldServerNode = viewCache.cachedServerSnap;
+ FViewCache *newViewCache = [viewCache updateServerSnap:oldServerNode.indexedNode
+ isComplete:(oldServerNode.isFullyInitialized || path.isEmpty)
+ isFiltered:oldServerNode.isFiltered];
+ return [self generateEventCacheAfterServerEvent:newViewCache path:path writesCache:writesCache source:[FNoCompleteChildSource instance] accumulator:accumulator];
+}
+
+@end
diff --git a/Firebase/Database/FViewProcessorResult.h b/Firebase/Database/FViewProcessorResult.h
new file mode 100644
index 0000000..e211d19
--- /dev/null
+++ b/Firebase/Database/FViewProcessorResult.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>
+
+@class FViewCache;
+
+
+@interface FViewProcessorResult : NSObject
+@property (nonatomic, strong, readonly) FViewCache *viewCache;
+/**
+* List of FChanges.
+*/
+@property (nonatomic, strong, readonly) NSArray *changes;
+
+- (id) initWithViewCache:(FViewCache *)viewCache changes:(NSArray *)changes;
+@end
diff --git a/Firebase/Database/FViewProcessorResult.m b/Firebase/Database/FViewProcessorResult.m
new file mode 100644
index 0000000..3327888
--- /dev/null
+++ b/Firebase/Database/FViewProcessorResult.m
@@ -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 "FViewProcessorResult.h"
+#import "FViewCache.h"
+
+@interface FViewProcessorResult ()
+@property (nonatomic, strong, readwrite) FViewCache *viewCache;
+@property (nonatomic, strong, readwrite) NSArray *changes;
+@end
+
+@implementation FViewProcessorResult
+- (id) initWithViewCache:(FViewCache *)viewCache changes:(NSArray *)changes {
+ self = [super init];
+ if (self) {
+ self.viewCache = viewCache;
+ self.changes = changes;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Firebase-Prefix.pch b/Firebase/Database/Firebase-Prefix.pch
new file mode 100644
index 0000000..0158d95
--- /dev/null
+++ b/Firebase/Database/Firebase-Prefix.pch
@@ -0,0 +1,7 @@
+//
+// Prefix header for all source files of the 'Firebase' target in the 'Firebase' project
+//
+
+#ifdef __OBJC__
+ #import <Foundation/Foundation.h>
+#endif
diff --git a/Firebase/Database/FirebaseDatabase.podspec b/Firebase/Database/FirebaseDatabase.podspec
new file mode 100644
index 0000000..4db371e
--- /dev/null
+++ b/Firebase/Database/FirebaseDatabase.podspec
@@ -0,0 +1,48 @@
+# This podspec is not intended to be deployed. It is solely for the static
+# library framework build process at
+# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks
+
+Pod::Spec.new do |s|
+ s.name = 'FirebaseDatabase'
+ s.version = '4.0.0'
+ s.summary = 'Firebase Open Source Libraries for iOS.'
+
+ s.description = <<-DESC
+Simplify your iOS development, grow your user base, and monetize more effectively with Firebase.
+ DESC
+
+ s.homepage = 'https://firebase.google.com'
+ s.license = { :type => 'Apache', :file => '../../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the
+ # Firebase pod
+ s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s }
+ s.social_media_url = 'https://twitter.com/Firebase'
+ s.ios.deployment_target = '7.0'
+
+ s.source_files = '**/*.[mh]',
+ 'third_party/Wrap-leveldb/APLevelDB.mm',
+ 'third_party/SocketRocket/fbase64.c'
+ s.public_header_files =
+ 'Api/FirebaseDatabase.h',
+ 'Api/FIRDataEventType.h',
+ 'Api/FIRDataSnapshot.h',
+ 'Api/FIRDatabaseQuery.h',
+ 'Api/FIRDatabaseSwiftNameSupport.h',
+ 'Api/FIRMutableData.h',
+ 'Api/FIRServerValue.h',
+ 'Api/FIRTransactionResult.h',
+ 'Api/FIRDatabase.h',
+ 'FIRDatabaseReference.h'
+ s.library = 'c++'
+ s.library = 'icucore'
+ s.framework = 'CFNetwork'
+ s.framework = 'Security'
+ s.framework = 'SystemConfiguration'
+ s.dependency 'leveldb-library'
+# s.dependency 'FirebaseDev/Core'
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' +
+ 'FIRDatabase_VERSION=' + s.version.to_s }
+end
diff --git a/Firebase/Database/Info.plist b/Firebase/Database/Info.plist
new file mode 100644
index 0000000..c707a67
--- /dev/null
+++ b/Firebase/Database/Info.plist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.firebase.$(PRODUCT_NAME:rfc1034identifier)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>XXX_TAG_VERSION_XXX</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>XXX_TAG_VERSION_XXX</string>
+ <key>NSPrincipalClass</key>
+ <string></string>
+</dict>
+</plist>
diff --git a/Firebase/Database/Login/FAuthTokenProvider.h b/Firebase/Database/Login/FAuthTokenProvider.h
new file mode 100644
index 0000000..dca0026
--- /dev/null
+++ b/Firebase/Database/Login/FAuthTokenProvider.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 "FTypedefs.h"
+#import "FTypedefs_Private.h"
+
+@protocol FAuthTokenProvider <NSObject>
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback;
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener;
+
+@end
+
+@interface FAuthTokenProvider : NSObject
+
++ (id<FAuthTokenProvider>) authTokenProviderForApp:(id)app;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+@end
diff --git a/Firebase/Database/Login/FAuthTokenProvider.m b/Firebase/Database/Login/FAuthTokenProvider.m
new file mode 100644
index 0000000..e406ae7
--- /dev/null
+++ b/Firebase/Database/Login/FAuthTokenProvider.m
@@ -0,0 +1,162 @@
+/*
+ * 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 "FAuthTokenProvider.h"
+#import "FUtilities.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FIRNoopAuthTokenProvider.h"
+
+static NSString *const FIRAuthStateDidChangeInternalNotification = @"FIRAuthStateDidChangeInternalNotification";
+static NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey = @"FIRAuthStateDidChangeInternalNotificationTokenKey";
+
+
+/**
+ * This is a hack that defines all the methods we need from FIRFirebaseApp. At runtime we use reflection to get an
+ * actual instance of FIRFirebaseApp. Since protocols don't carry any runtime information and selectors are invoked
+ * by name we can write code against this protocol as long as the method signatures of FIRFirebaseApp don't change.
+ */
+@protocol FIRFirebaseAppLike <NSObject>
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(void (^)(NSString *_Nullable token, NSError *_Nullable error))callback;
+
+@end
+
+
+/**
+ * This is a hack that defines all the methods we need from FIRAuth.
+ */
+@protocol FIRFirebaseAuthLike <NSObject>
+
+- (id<FIRFirebaseAppLike>) app;
+
+@end
+
+/**
+ * This is a hack that copies the definitions of Firebear error codes. If the error codes change in the original code, this
+ * will break at runtime due to undefined behavior!
+ */
+typedef NS_ENUM(NSUInteger, FIRErrorCode) {
+ /*! @var FIRErrorCodeNoAuth
+ @brief Represents the case where an auth-related message was sent to a @c FIRFirebaseApp
+ instance which has no associated @c FIRAuth instance.
+ */
+ FIRErrorCodeNoAuth,
+
+ /*! @var FIRErrorCodeNoSignedInUser
+ @brief Represents the case where an attempt was made to fetch a token when there is no signed
+ in user.
+ */
+ FIRErrorCodeNoSignedInUser,
+};
+
+
+@interface FAuthStateListenerWrapper : NSObject
+
+@property (nonatomic, copy) fbt_void_nsstring listener;
+
+@property (nonatomic, weak) id<FIRFirebaseAppLike> app;
+
+@end
+
+@implementation FAuthStateListenerWrapper
+
+- (instancetype) initWithListener:(fbt_void_nsstring)listener app:(id<FIRFirebaseAppLike>)app {
+ self = [super init];
+ if (self != nil) {
+ self->_listener = listener;
+ self->_app = app;
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(authStateDidChangeNotification:)
+ name:FIRAuthStateDidChangeInternalNotification
+ object:nil];
+ }
+ return self;
+}
+
+- (void) authStateDidChangeNotification:(NSNotification *)notification {
+ id<FIRFirebaseAuthLike> auth = notification.object;
+ if (auth.app == self->_app) {
+ NSDictionary *userInfo = notification.userInfo;
+ NSString *token = userInfo[FIRAuthStateDidChangeInternalNotificationTokenKey];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ self.listener(token);
+ });
+ }
+}
+
+- (void) dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+@end
+
+
+@interface FIRFirebearAuthTokenProvider : NSObject <FAuthTokenProvider>
+
+@property (nonatomic, strong) id<FIRFirebaseAppLike> app;
+/** Strong references to the auth listeners as they are only weak in FIRFirebaseApp */
+@property (nonatomic, strong) NSMutableArray *authListeners;
+
+- (instancetype) initWithFirebaseApp:(id<FIRFirebaseAppLike>)app;
+
+@end
+
+@implementation FIRFirebearAuthTokenProvider
+
+- (instancetype) initWithFirebaseApp:(id<FIRFirebaseAppLike>)app {
+ self = [super init];
+ if (self != nil) {
+ self->_app = app;
+ self->_authListeners = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ // TODO: Don't fetch token if there is no current user
+ [self.app getTokenForcingRefresh:forceRefresh withCallback:^(NSString * _Nullable token, NSError * _Nullable error) {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ if (error != nil) {
+ if (error.code == FIRErrorCodeNoAuth) {
+ FFLog(@"I-RDB073001", @"Firebase Auth is not configured, not going to use authentication.");
+ callback(nil, nil);
+ } else if (error.code == FIRErrorCodeNoSignedInUser) {
+ // No signed in user is an expected case, callback as success with no token
+ callback(nil, nil);
+ } else {
+ callback(nil, error);
+ }
+ } else {
+ callback(token, nil);
+ }
+ });
+ }];
+}
+
+- (void) listenForTokenChanges:(_Nonnull fbt_void_nsstring)listener {
+ FAuthStateListenerWrapper *wrapper = [[FAuthStateListenerWrapper alloc] initWithListener:listener app:self.app];
+ [self.authListeners addObject:wrapper];
+}
+
+@end
+
+@implementation FAuthTokenProvider
+
++ (id<FAuthTokenProvider>) authTokenProviderForApp:(id)app {
+ return [[FIRFirebearAuthTokenProvider alloc] initWithFirebaseApp:app];
+}
+
+@end
diff --git a/Firebase/Database/Login/FIRNoopAuthTokenProvider.h b/Firebase/Database/Login/FIRNoopAuthTokenProvider.h
new file mode 100644
index 0000000..e27ddb4
--- /dev/null
+++ b/Firebase/Database/Login/FIRNoopAuthTokenProvider.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "FAuthTokenProvider.h"
+
+@interface FIRNoopAuthTokenProvider : NSObject <FAuthTokenProvider>
+
+@end
diff --git a/Firebase/Database/Login/FIRNoopAuthTokenProvider.m b/Firebase/Database/Login/FIRNoopAuthTokenProvider.m
new file mode 100644
index 0000000..8bf467b
--- /dev/null
+++ b/Firebase/Database/Login/FIRNoopAuthTokenProvider.m
@@ -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 "FIRNoopAuthTokenProvider.h"
+#import "FAuthTokenProvider.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@implementation FIRNoopAuthTokenProvider
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ callback(nil, nil);
+ });
+}
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener {
+ // no-op, because token never changes
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FCachePolicy.h b/Firebase/Database/Persistence/FCachePolicy.h
new file mode 100644
index 0000000..16b49fb
--- /dev/null
+++ b/Firebase/Database/Persistence/FCachePolicy.h
@@ -0,0 +1,41 @@
+/*
+ * 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 FCachePolicy <NSObject>
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries;
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck;
+- (float)percentOfQueriesToPruneAtOnce;
+- (NSUInteger)maxNumberOfQueriesToKeep;
+
+@end
+
+
+@interface FLRUCachePolicy : NSObject<FCachePolicy>
+
+@property (nonatomic, readonly) NSUInteger maxSize;
+
+- (id)initWithMaxSize:(NSUInteger)maxSize;
+
+@end
+
+@interface FNoCachePolicy : NSObject<FCachePolicy>
+
++ (FNoCachePolicy *)noCachePolicy;
+
+@end
diff --git a/Firebase/Database/Persistence/FCachePolicy.m b/Firebase/Database/Persistence/FCachePolicy.m
new file mode 100644
index 0000000..7da76ef
--- /dev/null
+++ b/Firebase/Database/Persistence/FCachePolicy.m
@@ -0,0 +1,79 @@
+/*
+ * 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 "FCachePolicy.h"
+
+@interface FLRUCachePolicy ()
+
+@property (nonatomic, readwrite) NSUInteger maxSize;
+
+@end
+
+static const NSUInteger kFServerUpdatesBetweenCacheSizeChecks = 1000;
+static const NSUInteger kFMaxNumberOfPrunableQueriesToKeep = 1000;
+static const float kFPercentOfQueriesToPruneAtOnce = 0.2f;
+
+@implementation FLRUCachePolicy
+
+- (id)initWithMaxSize:(NSUInteger)maxSize {
+ self = [super init];
+ if (self != nil) {
+ self->_maxSize = maxSize;
+ }
+ return self;
+}
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries {
+ return cacheSize > self.maxSize || numTrackedQueries > kFMaxNumberOfPrunableQueriesToKeep;
+}
+
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck {
+ return serverUpdatesSinceLastCheck > kFServerUpdatesBetweenCacheSizeChecks;
+}
+
+- (float)percentOfQueriesToPruneAtOnce {
+ return kFPercentOfQueriesToPruneAtOnce;
+}
+
+- (NSUInteger)maxNumberOfQueriesToKeep {
+ return kFMaxNumberOfPrunableQueriesToKeep;
+}
+
+@end
+
+@implementation FNoCachePolicy
+
++ (FNoCachePolicy *)noCachePolicy {
+ return [[FNoCachePolicy alloc] init];
+}
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries {
+ return NO;
+}
+
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck {
+ return NO;
+}
+
+- (float)percentOfQueriesToPruneAtOnce {
+ return 0;
+}
+
+- (NSUInteger)maxNumberOfQueriesToKeep {
+ return NSUIntegerMax;
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FLevelDBStorageEngine.h b/Firebase/Database/Persistence/FLevelDBStorageEngine.h
new file mode 100644
index 0000000..059a071
--- /dev/null
+++ b/Firebase/Database/Persistence/FLevelDBStorageEngine.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 "FStorageEngine.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FCompoundWrite.h"
+#import "FQuerySpec.h"
+
+@class FCacheNode;
+@class FTrackedQuery;
+@class FPruneForest;
+@class FRepoInfo;
+
+@interface FLevelDBStorageEngine : NSObject<FStorageEngine>
+
+- (id)initWithPath:(NSString *)path;
+
+- (void)runLegacyMigration:(FRepoInfo *)info;
+- (void)purgeEverything;
+
+@end
diff --git a/Firebase/Database/Persistence/FLevelDBStorageEngine.m b/Firebase/Database/Persistence/FLevelDBStorageEngine.m
new file mode 100644
index 0000000..4b324b8
--- /dev/null
+++ b/Firebase/Database/Persistence/FLevelDBStorageEngine.m
@@ -0,0 +1,717 @@
+/*
+ * 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 "FLevelDBStorageEngine.h"
+
+#import "APLevelDB.h"
+#import "FSnapshotUtilities.h"
+#import "FWriteRecord.h"
+#import "FTrackedQuery.h"
+#import "FQueryParams.h"
+#import "FEmptyNode.h"
+#import "FPruneForest.h"
+#import "FUtilities.h"
+#import "FPendingPut.h" // For legacy migration
+
+@interface FLevelDBStorageEngine ()
+
+@property (nonatomic, strong) NSString *basePath;
+@property (nonatomic, strong) APLevelDB *writesDB;
+@property (nonatomic, strong) APLevelDB *serverCacheDB;
+
+@end
+
+// WARNING: If you change this, you need to write a migration script
+static NSString * const kFPersistenceVersion = @"1";
+
+static NSString * const kFServerDBPath = @"server_data";
+static NSString * const kFWritesDBPath = @"writes";
+
+static NSString * const kFUserWriteId = @"id";
+static NSString * const kFUserWritePath = @"path";
+static NSString * const kFUserWriteOverwrite = @"o";
+static NSString * const kFUserWriteMerge = @"m";
+
+static NSString * const kFTrackedQueryId = @"id";
+static NSString * const kFTrackedQueryPath = @"path";
+static NSString * const kFTrackedQueryParams = @"p";
+static NSString * const kFTrackedQueryLastUse = @"lu";
+static NSString * const kFTrackedQueryIsComplete = @"c";
+static NSString * const kFTrackedQueryIsActive = @"a";
+
+static NSString * const kFServerCachePrefix = @"/server_cache/";
+// '~' is the last non-control character in the ASCII table until 127
+// We wan't the entire range of thing stored in the DB
+static NSString * const kFServerCacheRangeEnd = @"/server_cache~";
+static NSString * const kFTrackedQueriesPrefix = @"/tracked_queries/";
+static NSString * const kFTrackedQueryKeysPrefix = @"/tracked_query_keys/";
+
+// Failed to load JSON because a valid JSON turns out to be NaN while deserializing
+static const NSInteger kFNanFailureCode = 3840;
+
+static NSString* writeRecordKey(NSUInteger writeId) {
+ return [NSString stringWithFormat:@"%lu", (unsigned long)(writeId)];
+}
+
+static NSString* serverCacheKey(FPath *path) {
+ return [NSString stringWithFormat:@"%@%@", kFServerCachePrefix, ([path toStringWithTrailingSlash])];
+}
+
+static NSString* trackedQueryKey(NSUInteger trackedQueryId) {
+ return [NSString stringWithFormat:@"%@%lu", kFTrackedQueriesPrefix, (unsigned long)trackedQueryId];
+}
+
+static NSString* trackedQueryKeysKeyPrefix(NSUInteger trackedQueryId) {
+ return [NSString stringWithFormat:@"%@%lu/", kFTrackedQueryKeysPrefix, (unsigned long)trackedQueryId];
+}
+
+static NSString* trackedQueryKeysKey(NSUInteger trackedQueryId, NSString *key) {
+ return [NSString stringWithFormat:@"%@%lu/%@", kFTrackedQueryKeysPrefix, (unsigned long)trackedQueryId, key];
+}
+
+@implementation FLevelDBStorageEngine
+#pragma mark - Constructors
+
+- (id)initWithPath:(NSString*)dbPath
+{
+ self = [super init];
+ if (self) {
+ self.basePath = [[FLevelDBStorageEngine firebaseDir] stringByAppendingPathComponent:dbPath];
+ /* For reference:
+ serverDataDB = [aPersistence createDbByName:@"server_data"];
+ FPangolinDB *completenessDb = [aPersistence createDbByName:@"server_complete"];
+ */
+ [FLevelDBStorageEngine ensureDir:self.basePath markAsDoNotBackup:YES];
+ [self runMigration];
+ [self openDatabases];
+ }
+ return self;
+}
+
+- (void)runMigration {
+ // Currently we're at version 1, so all we need to do is write that to a file
+ NSString *versionFile = [self.basePath stringByAppendingPathComponent:@"version"];
+ NSError *error;
+ NSString *oldVersion = [NSString stringWithContentsOfFile:versionFile encoding:NSUTF8StringEncoding error:&error];
+ if (!oldVersion) {
+ // This is probably fine, we don't have a version file yet
+ BOOL success = [kFPersistenceVersion writeToFile:versionFile atomically:NO encoding:NSUTF8StringEncoding error:&error];
+ if (!success) {
+ FFWarn(@"I-RDB076001", @"Failed to write version for database: %@", error);
+ }
+ } else if ([oldVersion isEqualToString:kFPersistenceVersion]) {
+ // Everythings fine no need for migration
+ } else {
+ // If we add more versions in the future, we need to run migration here
+ [NSException raise:NSInternalInconsistencyException format:@"Unrecognized database version: %@", oldVersion];
+ }
+}
+
+- (void)runLegacyMigration:(FRepoInfo *)info {
+ NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDir = [dirPaths objectAtIndex:0];
+ NSString *firebaseDir = [documentsDir stringByAppendingPathComponent:@"firebase"];
+ NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", info.host, info.namespace];
+ NSString *legacyBaseDir = [NSString stringWithFormat:@"%@/1/%@/v1", firebaseDir, repoHashString];
+ if ([[NSFileManager defaultManager] fileExistsAtPath:legacyBaseDir]) {
+ FFWarn(@"I-RDB076002", @"Legacy database found, migrating...");
+ // We only need to migrate writes
+ NSError *error = nil;
+ APLevelDB *writes = [APLevelDB levelDBWithPath:[legacyBaseDir stringByAppendingPathComponent:@"outstanding_puts"] error:&error];
+ if (writes != nil) {
+ __block NSUInteger numberOfWritesRestored = 0;
+ // Maybe we could use write batches, but what the heck, I'm sure it'll go fine :P
+ [writes enumerateKeysAndValuesAsData:^(NSString *key, NSData *data, BOOL *stop) {
+ id pendingPut = [NSKeyedUnarchiver unarchiveObjectWithData:data];
+ if ([pendingPut isKindOfClass:[FPendingPut class]]) {
+ FPendingPut *put = pendingPut;
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:put.data priority:put.priority];
+ [self saveUserOverwrite:newNode atPath:put.path writeId:[key integerValue]];
+ numberOfWritesRestored++;
+ } else if ([pendingPut isKindOfClass:[FPendingPutPriority class]]) {
+ // This is for backwards compatibility. Older clients will save FPendingPutPriority. New ones will need to read it and translate.
+ FPendingPutPriority *putPriority = pendingPut;
+ FPath *priorityPath = [putPriority.path childFromString:@".priority"];
+ id<FNode> newNode = [FSnapshotUtilities nodeFrom:putPriority.priority priority:nil];
+ [self saveUserOverwrite:newNode atPath:priorityPath writeId:[key integerValue]];
+ numberOfWritesRestored++;
+ } else if ([pendingPut isKindOfClass:[FPendingUpdate class]]) {
+ FPendingUpdate *update = pendingPut;
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:update.data];
+ [self saveUserMerge:merge atPath:update.path writeId:[key integerValue]];
+ numberOfWritesRestored++;
+ } else {
+ FFWarn(@"I-RDB076003", @"Failed to migrate legacy write, meh!");
+ }
+ }];
+ FFWarn(@"I-RDB076004", @"Migrated %lu writes", (unsigned long)numberOfWritesRestored);
+ [writes close];
+ FFWarn(@"I-RDB076005", @"Deleting legacy database...");
+ BOOL success = [[NSFileManager defaultManager] removeItemAtPath:legacyBaseDir error:&error];
+ if (!success) {
+ FFWarn(@"I-RDB076006", @"Failed to delete legacy database: %@", error);
+ } else {
+ FFWarn(@"I-RDB076007", @"Finished migrating legacy database.");
+ }
+ } else {
+ FFWarn(@"I-RDB076008", @"Failed to migrate old database: %@", error);
+ }
+ }
+}
+
+- (void)openDatabases {
+ self.serverCacheDB = [self createDB:kFServerDBPath];
+ self.writesDB = [self createDB:kFWritesDBPath];
+}
+
+- (void)purgeEverything {
+ [self close];
+ [@[kFServerDBPath, kFWritesDBPath]
+ enumerateObjectsUsingBlock:^(NSString *dbPath, NSUInteger idx, BOOL *stop) {
+ NSString *path = [self.basePath stringByAppendingPathComponent:dbPath];
+ NSError *error;
+ FFDebug(@"I-RDB076009", @"Deleting database at path %@", path);
+ BOOL success = [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
+ if (!success) {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to delete database files: %@", error];
+ }
+ }];
+
+ [self openDatabases];
+}
+
+- (void)close {
+ // autoreleasepool will cause deallocation which will close the DB
+ @autoreleasepool {
+ [self.serverCacheDB close];
+ self.serverCacheDB = nil;
+ [self.writesDB close];
+ self.writesDB = nil;
+ }
+}
+
++ (NSString *) firebaseDir {
+#if TARGET_OS_IPHONE
+ NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDir = [dirPaths objectAtIndex:0];
+ return [documentsDir stringByAppendingPathComponent:@"firebase"];
+#else // this must be OSX then
+ return [NSHomeDirectory() stringByAppendingPathComponent:@".firebase"];
+#endif
+}
+
+- (APLevelDB *)createDB:(NSString *)name {
+ NSError *err = nil;
+ NSString *path = [self.basePath stringByAppendingPathComponent:name];
+ APLevelDB *db = [APLevelDB levelDBWithPath:path error:&err];
+ if(err) {
+ NSString *reason = [NSString stringWithFormat:@"Error initializing persistence: %@", [err description]];
+ @throw [NSException exceptionWithName:@"FirebaseDatabasePersistenceFailure" reason:reason userInfo:nil];
+ }
+ return db;
+}
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ NSDictionary *write =
+ @{ kFUserWriteId: @(writeId),
+ kFUserWritePath: [path toStringWithTrailingSlash],
+ kFUserWriteOverwrite: [node valForExport:YES] };
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:write options:0 error:&error];
+ NSAssert(data, @"Failed to serialize user overwrite: %@, (Error: %@)", write, error);
+ [self.writesDB setData:data forKey:writeRecordKey(writeId)];
+}
+
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ NSDictionary *write =
+ @{ kFUserWriteId: @(writeId),
+ kFUserWritePath: [path toStringWithTrailingSlash],
+ kFUserWriteMerge: [merge valForExport:YES] };
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:write options:0 error:&error];
+ NSAssert(data, @"Failed to serialize user merge: %@ (Error: %@)", write, error);
+ [self.writesDB setData:data forKey:writeRecordKey(writeId)];
+}
+
+- (void)removeUserWrite:(NSUInteger)writeId {
+ [self.writesDB removeKey:writeRecordKey(writeId)];
+}
+
+- (void)removeAllUserWrites {
+ __block NSUInteger count = 0;
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.writesDB beginWriteBatch];
+ [self.writesDB enumerateKeys:^(NSString *key, BOOL *stop) {
+ [batch removeKey:key];
+ count++;
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076010", @"Failed to remove all users writes on disk!");
+ } else {
+ FFDebug(@"I-RDB076011", @"Removed %lu writes in %fms", (unsigned long)count, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (NSArray *)userWrites {
+ NSDate *date = [NSDate date];
+ NSMutableArray *writes = [NSMutableArray array];
+ [self.writesDB enumerateKeysAndValuesAsData:^(NSString *key, NSData *data, BOOL *stop) {
+ NSError *error = nil;
+ NSDictionary *writeJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
+ if (writeJSON == nil) {
+ if (error.code == kFNanFailureCode) {
+ FFWarn(@"I-RDB076012", @"Failed to deserialize write (%@), likely because of out of range doubles (Error: %@)",
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],
+ error);
+ FFWarn(@"I-RDB076013", @"Removing failed write with key %@", key);
+ [self.writesDB removeKey:key];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialize write: %@", error];
+ }
+ } else {
+ NSInteger writeId = ((NSNumber *)writeJSON[kFUserWriteId]).integerValue;
+ FPath *path = [FPath pathWithString:writeJSON[kFUserWritePath]];
+ FWriteRecord *writeRecord;
+ if (writeJSON[kFUserWriteMerge] != nil) {
+ // It's a merge
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:writeJSON[kFUserWriteMerge]];
+ writeRecord = [[FWriteRecord alloc] initWithPath:path merge:merge writeId:writeId];
+ } else {
+ // It's an overwrite
+ NSAssert(writeJSON[kFUserWriteOverwrite] != nil, @"Persisted write did not contain merge or overwrite!");
+ id<FNode> node = [FSnapshotUtilities nodeFrom:writeJSON[kFUserWriteOverwrite]];
+ writeRecord = [[FWriteRecord alloc] initWithPath:path overwrite:node writeId:writeId visible:YES];
+ }
+ [writes addObject:writeRecord];
+ }
+ }];
+ // Make sure writes are sorted
+ [writes sortUsingComparator:^NSComparisonResult(FWriteRecord *one, FWriteRecord *two) {
+ if (one.writeId < two.writeId) {
+ return NSOrderedAscending;
+ } else if (one.writeId > two.writeId) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ }];
+ FFDebug(@"I-RDB076014", @"Loaded %lu writes in %fms", (unsigned long)writes.count, [date timeIntervalSinceNow]*-1000);
+ return writes;
+}
+
+- (id<FNode>)serverCacheAtPath:(FPath *)path {
+ NSDate *start = [NSDate date];
+ id data = [self internalNestedDataForPath:path];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:data];
+ FFDebug(@"I-RDB076015", @"Loaded node with %d children at %@ in %fms", [node numChildren], path, [start timeIntervalSinceNow]*-1000);
+ return node;
+}
+
+- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path {
+ NSDate *start = [NSDate date];
+ __block id<FNode> node = [FEmptyNode emptyNode];
+ [keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ id data = [self internalNestedDataForPath:[path childFromString:key]];
+ node = [node updateImmediateChild:key withNewChild:[FSnapshotUtilities nodeFrom:data]];
+ }];
+ FFDebug(@"I-RDB076016", @"Loaded node with %d children for %lu keys at %@ in %fms", [node numChildren], (unsigned long)keys.count, path, [start timeIntervalSinceNow]*-1000);
+ return node;
+}
+
+- (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge {
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ // Remove any leaf nodes that might be higher up
+ [self removeAllLeafNodesOnPath:path batch:batch];
+ __block NSUInteger counter = 0;
+ if (merge) {
+ // remove any children that exist
+ [node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) {
+ FPath *childPath = [path childFromString:childKey];
+ [self removeAllWithPrefix:serverCacheKey(childPath) batch:batch database:self.serverCacheDB];
+ [self saveNodeInternal:childNode atPath:childPath batch:batch counter:&counter];
+ }];
+ } else {
+ // remove everything
+ [self removeAllWithPrefix:serverCacheKey(path) batch:batch database:self.serverCacheDB];
+ [self saveNodeInternal:node atPath:path batch:batch counter:&counter];
+ }
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076017", @"Failed to update server cache on disk!");
+ } else {
+ FFDebug(@"I-RDB076018", @"Saved %lu leaf nodes for overwrite in %fms", (unsigned long)counter, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path {
+ NSDate *start = [NSDate date];
+ __block NSUInteger counter = 0;
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ // Remove any leaf nodes that might be higher up
+ [self removeAllLeafNodesOnPath:path batch:batch];
+ [merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
+ FPath *childPath = [path child:relativePath];
+ [self removeAllWithPrefix:serverCacheKey(childPath) batch:batch database:self.serverCacheDB];
+ [self saveNodeInternal:node atPath:childPath batch:batch counter:&counter];
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076019", @"Failed to update server cache on disk!");
+ } else {
+ FFDebug(@"I-RDB076020", @"Saved %lu leaf nodes for merge in %fms", (unsigned long)counter, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)saveNodeInternal:(id<FNode>)node atPath:(FPath *)path batch:(id<APLevelDBWriteBatch>)batch counter:(NSUInteger *)counter {
+ id data = [node valForExport:YES];
+ if(data != nil && ![data isKindOfClass:[NSNull class]]) {
+ [self internalSetNestedData:data forKey:serverCacheKey(path) withBatch:batch counter:counter];
+ }
+}
+
+- (NSUInteger)serverCacheEstimatedSizeInBytes {
+ // Use the exact size, because for pruning the approximate size can lead to weird situations where we prune everything
+ // because no compaction is ever run
+ return [self.serverCacheDB exactSizeFrom:kFServerCachePrefix to:kFServerCacheRangeEnd];
+}
+
+- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path {
+ // TODO: be more intelligent, don't scan entire database...
+
+ __block NSUInteger pruned = 0;
+ __block NSUInteger kept = 0;
+ NSDate *start = [NSDate date];
+
+ NSString *prefix = serverCacheKey(path);
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+
+ [self.serverCacheDB enumerateKeysWithPrefix:prefix usingBlock:^(NSString *dbKey, BOOL *stop) {
+ NSString *pathStr = [dbKey substringFromIndex:prefix.length];
+ FPath *relativePath = [[FPath alloc] initWith:pathStr];
+ if ([pruneForest shouldPruneUnkeptDescendantsAtPath:relativePath]) {
+ pruned++;
+ [batch removeKey:dbKey];
+ } else {
+ kept++;
+ }
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076021", @"Failed to prune cache on disk!");
+ } else {
+ FFDebug(@"I-RDB076022", @"Pruned %lu paths, kept %lu paths in %fms", (unsigned long)pruned, (unsigned long)kept, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+#pragma mark - Tracked Queries
+
+- (NSArray *)loadTrackedQueries {
+ NSDate *date = [NSDate date];
+ NSMutableArray *trackedQueries = [NSMutableArray array];
+ [self.serverCacheDB enumerateKeysWithPrefix:kFTrackedQueriesPrefix asData:^(NSString *key, NSData *data, BOOL *stop) {
+ NSError *error = nil;
+ NSDictionary *queryJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
+ if (queryJSON == nil) {
+ if (error.code == kFNanFailureCode) {
+ FFWarn(@"I-RDB076023", @"Failed to deserialize tracked query (%@), likely because of out of range doubles (Error: %@)",
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],
+ error);
+ FFWarn(@"I-RDB076024", @"Removing failed tracked query with key %@", key);
+ [self.serverCacheDB removeKey:key];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialize tracked query: %@", error];
+ }
+ } else {
+ NSUInteger queryId = ((NSNumber *)queryJSON[kFTrackedQueryId]).unsignedIntegerValue;
+ FPath *path = [FPath pathWithString:queryJSON[kFTrackedQueryPath]];
+ FQueryParams *params = [FQueryParams fromQueryObject:queryJSON[kFTrackedQueryParams]];
+ FQuerySpec *query = [[FQuerySpec alloc] initWithPath:path params:params];
+ BOOL isComplete = [queryJSON[kFTrackedQueryIsComplete] boolValue];
+ BOOL isActive = [queryJSON[kFTrackedQueryIsActive] boolValue];
+ NSTimeInterval lastUse = [queryJSON[kFTrackedQueryLastUse] doubleValue];
+
+ FTrackedQuery *trackedQuery = [[FTrackedQuery alloc] initWithId:queryId
+ query:query
+ lastUse:lastUse
+ isActive:isActive
+ isComplete:isComplete];
+
+ [trackedQueries addObject:trackedQuery];
+ }
+ }];
+ FFDebug(@"I-RDB076025", @"Loaded %lu tracked queries in %fms", (unsigned long)trackedQueries.count, [date timeIntervalSinceNow]*-1000);
+ return trackedQueries;
+}
+
+- (void)removeTrackedQuery:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ [batch removeKey:trackedQueryKey(queryId)];
+ __block NSUInteger keyCount = 0;
+ [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) usingBlock:^(NSString *key, BOOL *stop) {
+ [batch removeKey:key];
+ keyCount++;
+ }];
+
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076026", @"Failed to remove tracked query on disk!");
+ } else {
+ FFDebug(@"I-RDB076027", @"Removed query with id %lu (and removed %lu keys) in %fms",
+ (unsigned long)queryId,
+ (unsigned long)keyCount,
+ [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)saveTrackedQuery:(FTrackedQuery *)query {
+ NSDate *start = [NSDate date];
+ NSDictionary *trackedQuery =
+ @{
+ kFTrackedQueryId: @(query.queryId),
+ kFTrackedQueryPath: [query.query.path toStringWithTrailingSlash],
+ kFTrackedQueryParams: [query.query.params wireProtocolParams],
+ kFTrackedQueryLastUse: @(query.lastUse),
+ kFTrackedQueryIsComplete: @(query.isComplete),
+ kFTrackedQueryIsActive: @(query.isActive)
+ };
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:trackedQuery options:0 error:&error];
+ NSAssert(data, @"Failed to serialize tracked query (Error: %@)", error);
+ [self.serverCacheDB setData:data forKey:trackedQueryKey(query.queryId)];
+ FFDebug(@"I-RDB076028", @"Saved tracked query %lu in %fms", (unsigned long)query.queryId, [start timeIntervalSinceNow]*-1000);
+}
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ __block NSUInteger removed = 0;
+ __block NSUInteger added = 0;
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ NSMutableSet *seenKeys = [NSMutableSet set];
+ // First, delete any keys that might be stored and are not part of the current keys
+ [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) asStrings:^(NSString *dbKey, NSString *actualKey, BOOL *stop) {
+ if ([keys containsObject:actualKey]) {
+ // Already in DB
+ [seenKeys addObject:actualKey];
+ } else {
+ // Not part of set, delete key
+ [batch removeKey:dbKey];
+ removed++;
+ }
+ }];
+
+ // Next add any keys that are missing in the database
+ [keys enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
+ if (![seenKeys containsObject:childKey]) {
+ [batch setString:childKey forKey:trackedQueryKeysKey(queryId, childKey)];
+ added++;
+ }
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076029", @"Failed to set tracked queries on disk!");
+ } else {
+ FFDebug(@"I-RDB076030", @"Set %lu tracked keys (%lu added, %lu removed) for query %lu in %fms",
+ (unsigned long)keys.count,
+ (unsigned long)added,
+ (unsigned long)removed,
+ (unsigned long)queryId,
+ [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ id<APLevelDBWriteBatch> batch = [self.serverCacheDB beginWriteBatch];
+ [removed enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ [batch removeKey:trackedQueryKeysKey(queryId, key)];
+ }];
+ [added enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ [batch setString:key forKey:trackedQueryKeysKey(queryId, key)];
+ }];
+ BOOL success = [batch commit];
+ if (!success) {
+ FFWarn(@"I-RDB076031", @"Failed to update tracked queries on disk!");
+ } else {
+ FFDebug(@"I-RDB076032", @"Added %lu tracked keys, removed %lu for query %lu in %fms", (unsigned long)added.count, (unsigned long)removed.count, (unsigned long)queryId, [start timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId {
+ NSDate *start = [NSDate date];
+ NSMutableSet *set = [NSMutableSet set];
+ [self.serverCacheDB enumerateKeysWithPrefix:trackedQueryKeysKeyPrefix(queryId) asStrings:^(NSString *dbKey, NSString *actualKey, BOOL *stop) {
+ [set addObject:actualKey];
+ }];
+ FFDebug(@"I-RDB076033", @"Loaded %lu tracked keys for query %lu in %fms", (unsigned long)set.count, (unsigned long)queryId, [start timeIntervalSinceNow]*-1000);
+ return set;
+}
+
+#pragma mark - Internal methods
+
+- (void)removeAllLeafNodesOnPath:(FPath *)path batch:(id<APLevelDBWriteBatch>)batch {
+ while (!path.isEmpty) {
+ [batch removeKey:serverCacheKey(path)];
+ path = [path parent];
+ }
+ // Make sure to delete any nodes at the root
+ [batch removeKey:serverCacheKey([FPath empty])];
+}
+
+- (void)removeAllWithPrefix:(NSString *)prefix batch:(id<APLevelDBWriteBatch>)batch database:(APLevelDB *)database {
+ assert(prefix != nil);
+
+ [database enumerateKeysWithPrefix:prefix usingBlock:^(NSString *key, BOOL *stop) {
+ [batch removeKey:key];
+ }];
+}
+
+#pragma mark - Internal helper methods
+
+- (void)internalSetNestedData:(id)value forKey:(NSString *)key withBatch:(id<APLevelDBWriteBatch>)batch counter:(NSUInteger *)counter {
+ if([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dictionary = value;
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id childKey, id obj, BOOL *stop) {
+ assert(obj != nil);
+ NSString* childPath = [NSString stringWithFormat:@"%@%@/", key, childKey];
+ [self internalSetNestedData:obj forKey:childPath withBatch:batch counter:counter];
+ }];
+ }
+ else {
+ NSData *data = [self serializePrimitive:value];
+ [batch setData:data forKey:key];
+ (*counter)++;
+ }
+}
+
+- (id)internalNestedDataForPath:(FPath *)path {
+ NSAssert(path != nil, @"Path was nil!");
+
+ NSString *baseKey = serverCacheKey(path);
+
+ // HACK to make sure iter is freed now to avoid race conditions (if self.db is deleted before iter, you get an access violation).
+ @autoreleasepool {
+ APLevelDBIterator* iter = [APLevelDBIterator iteratorWithLevelDB:self.serverCacheDB];
+
+ [iter seekToKey:baseKey];
+ if (iter.key == nil || ![iter.key hasPrefix:baseKey]) {
+ // No data.
+ return nil;
+ } else {
+ return [self internalNestedDataFromIterator:iter andKeyPrefix:baseKey];
+ }
+ }
+}
+
+- (id) internalNestedDataFromIterator:(APLevelDBIterator*)iterator andKeyPrefix:(NSString*)prefix {
+ NSString* key = iterator.key;
+
+ if ([key isEqualToString:prefix]) {
+ id result = [self deserializePrimitive:iterator.valueAsData];
+ [iterator nextKey];
+ return result;
+ } else {
+ NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
+ while (key != nil && [key hasPrefix:prefix]) {
+ NSString *relativePath = [key substringFromIndex:prefix.length];
+ NSArray* pathPieces = [relativePath componentsSeparatedByString:@"/"];
+ assert(pathPieces.count > 0);
+ NSString *childName = pathPieces[0];
+ NSString *childPath = [NSString stringWithFormat:@"%@%@/", prefix, childName];
+ id childValue = [self internalNestedDataFromIterator:iterator andKeyPrefix:childPath];
+ [dict setValue:childValue forKey:childName];
+
+ key = iterator.key;
+ }
+ return dict;
+ }
+}
+
+
+- (NSData*) serializePrimitive:(id)value {
+ // HACK: The built-in serialization only works on dicts and arrays. So we create an array and then strip off
+ // the leading / trailing byte (the [ and ]).
+ NSError *error = nil;
+ NSData *data = [NSJSONSerialization dataWithJSONObject:@[value] options:0 error:&error];
+ NSAssert(data, @"Failed to serialize primitive: %@", error);
+
+ return [data subdataWithRange:NSMakeRange(1, data.length - 2)];
+}
+
+- (id)fixDoubleParsing:(id)value {
+ // The parser for double values in JSONSerialization at the root takes some short-cuts and delivers wrong results
+ // (wrong rounding) for some double values, including 2.47. Because we use the exact bytes for hashing on the server
+ // this will lead to hash mismatches. The parser of NSNumber seems to be more in line with what the server expects,
+ // so we use that here
+ if ([value isKindOfClass:[NSNumber class]]) {
+ CFNumberType type = CFNumberGetType((CFNumberRef)value);
+ if (type == kCFNumberDoubleType || type == kCFNumberFloatType) {
+ // The NSJSON parser returns all numbers as double values, even those that contain no exponent. To
+ // make sure that the String conversion below doesn't unexpectedly reduce precision, we make sure that
+ // our number is indeed not an integer.
+ if ((double)(long long)[value doubleValue] != [value doubleValue]) {
+ NSString *doubleString = [value stringValue];
+ return [NSNumber numberWithDouble:[doubleString doubleValue]];
+ }
+ }
+ }
+ return value;
+}
+
+- (id) deserializePrimitive:(NSData*)data {
+ NSError *error = nil;
+ id result = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
+ if (result != nil) {
+ return [self fixDoubleParsing:result];
+ } else {
+ if (error.code == kFNanFailureCode) {
+ FFWarn(@"I-RDB076034", @"Failed to load primitive %@, likely because doubles where out of range (Error: %@)",
+ [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], error);
+ return [NSNull null];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to deserialiaze primitive: %@", error];
+ return nil;
+ }
+ }
+
+}
+
++ (void)ensureDir:(NSString*)path markAsDoNotBackup:(BOOL)markAsDoNotBackup {
+ NSError* error;
+ BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&error];
+ if (!success) {
+ @throw [NSException exceptionWithName:@"FailedToCreatePersistenceDir" reason:@"Failed to create persistence directory." userInfo:@{ @"path": path }];
+ }
+
+ if (markAsDoNotBackup) {
+ NSURL *firebaseDirURL = [NSURL fileURLWithPath:path];
+ success = [firebaseDirURL setResourceValue:@YES
+ forKey:NSURLIsExcludedFromBackupKey
+ error:&error];
+ if (!success) {
+ FFWarn(@"I-RDB076035", @"Failed to mark firebase database folder as do not backup: %@", error);
+ [NSException raise:@"Error marking as do not backup" format:@"Failed to mark folder %@ as do not backup", firebaseDirURL];
+ }
+ }
+}
+
+
+@end
diff --git a/Firebase/Database/Persistence/FPendingPut.h b/Firebase/Database/Persistence/FPendingPut.h
new file mode 100644
index 0000000..0d8de55
--- /dev/null
+++ b/Firebase/Database/Persistence/FPendingPut.h
@@ -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 <Foundation/Foundation.h>
+#import "FPath.h"
+
+// These are all legacy classes and are used to migrate older persistence data base to newer ones
+// These classes should not be used in newer code
+
+@interface FPendingPut : NSObject<NSCoding>
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) id data;
+@property (nonatomic, strong) id priority;
+
+- (id) initWithPath:(FPath*)aPath andData:(id)aData andPriority:aPriority;
+- (void)encodeWithCoder:(NSCoder *)aCoder;
+- (id)initWithCoder:(NSCoder *)aDecoder;
+@end
+
+
+@interface FPendingPutPriority : NSObject<NSCoding>
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) id priority;
+
+- (id) initWithPath:(FPath*)aPath andPriority:(id)aPriority;
+- (void)encodeWithCoder:(NSCoder *)aCoder;
+- (id)initWithCoder:(NSCoder *)aDecoder;
+
+@end
+
+
+@interface FPendingUpdate : NSObject<NSCoding>
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) NSDictionary* data;
+
+- (id) initWithPath:(FPath*)aPath andData:(NSDictionary*)aData;
+- (void)encodeWithCoder:(NSCoder *)aCoder;
+- (id)initWithCoder:(NSCoder *)aDecoder;
+@end
diff --git a/Firebase/Database/Persistence/FPendingPut.m b/Firebase/Database/Persistence/FPendingPut.m
new file mode 100644
index 0000000..12be825
--- /dev/null
+++ b/Firebase/Database/Persistence/FPendingPut.m
@@ -0,0 +1,112 @@
+/*
+ * 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 "FPendingPut.h"
+
+@implementation FPendingPut
+
+@synthesize path;
+@synthesize data;
+
+- (id) initWithPath:(FPath *)aPath andData:(id)aData andPriority:(id)aPriority {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.data = aData;
+ self.priority = aPriority;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[self.path description] forKey:@"path"];
+ [aCoder encodeObject:self.data forKey:@"data"];
+ [aCoder encodeObject:self.priority forKey:@"priority"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super init];
+ if(self) {
+ self.path = [[FPath alloc] initWith:[aDecoder decodeObjectForKey:@"path"]];
+ self.data = [aDecoder decodeObjectForKey:@"data"];
+ self.priority = [aDecoder decodeObjectForKey:@"priority"];
+ }
+ return self;
+}
+
+@end
+
+
+@implementation FPendingPutPriority
+
+@synthesize path;
+@synthesize priority;
+
+- (id) initWithPath:(FPath *)aPath andPriority:(id)aPriority {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.priority = aPriority;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[self.path description] forKey:@"path"];
+ [aCoder encodeObject:self.priority forKey:@"priority"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super init];
+ if(self) {
+ self.path = [[FPath alloc] initWith:[aDecoder decodeObjectForKey:@"path"]];
+ self.priority = [aDecoder decodeObjectForKey:@"priority"];
+ }
+ return self;
+}
+
+@end
+
+
+@implementation FPendingUpdate
+
+@synthesize path;
+@synthesize data;
+
+- (id) initWithPath:(FPath *)aPath andData:(id)aData {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.data = aData;
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+ [aCoder encodeObject:[self.path description] forKey:@"path"];
+ [aCoder encodeObject:self.data forKey:@"data"];
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super init];
+ if(self) {
+ self.path = [[FPath alloc] initWith:[aDecoder decodeObjectForKey:@"path"]];
+ self.data = [aDecoder decodeObjectForKey:@"data"];
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FPersistenceManager.h b/Firebase/Database/Persistence/FPersistenceManager.h
new file mode 100644
index 0000000..a3688b3
--- /dev/null
+++ b/Firebase/Database/Persistence/FPersistenceManager.h
@@ -0,0 +1,52 @@
+/*
+ * 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 "FCompoundWrite.h"
+#import "FQuerySpec.h"
+#import "FRepoInfo.h"
+#import "FStorageEngine.h"
+#import "FCachePolicy.h"
+#import "FCacheNode.h"
+
+@interface FPersistenceManager : NSObject
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine cachePolicy:(id<FCachePolicy>)cachePolicy;
+- (void)close;
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)removeUserWrite:(NSUInteger)writeId;
+- (void)removeAllUserWrites;
+- (NSArray *)userWrites;
+
+- (FCacheNode *)serverCacheForQuery:(FQuerySpec *)spec;
+- (void)updateServerCacheWithNode:(id<FNode>)node forQuery:(FQuerySpec *)spec;
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path;
+
+- (void)applyUserWrite:(id<FNode>)write toServerCacheAtPath:(FPath *)path;
+- (void)applyUserMerge:(FCompoundWrite *)merge toServerCacheAtPath:(FPath *)path;
+
+- (void)setQueryComplete:(FQuerySpec *)spec;
+- (void)setQueryActive:(FQuerySpec *)spec;
+- (void)setQueryInactive:(FQuerySpec *)spec;
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQuery:(FQuerySpec *)query;
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQuery:(FQuerySpec *)query;
+
+@end
diff --git a/Firebase/Database/Persistence/FPersistenceManager.m b/Firebase/Database/Persistence/FPersistenceManager.m
new file mode 100644
index 0000000..fb38192
--- /dev/null
+++ b/Firebase/Database/Persistence/FPersistenceManager.m
@@ -0,0 +1,190 @@
+/*
+ * 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 "FPersistenceManager.h"
+#import "FLevelDBStorageEngine.h"
+#import "FCacheNode.h"
+#import "FIndexedNode.h"
+#import "FTrackedQueryManager.h"
+#import "FTrackedQuery.h"
+#import "FUtilities.h"
+#import "FPruneForest.h"
+#import "FClock.h"
+
+@interface FPersistenceManager ()
+
+@property (nonatomic, strong) id<FStorageEngine> storageEngine;
+@property (nonatomic, strong) id<FCachePolicy> cachePolicy;
+@property (nonatomic, strong) FTrackedQueryManager *trackedQueryManager;
+@property (nonatomic) NSUInteger serverCacheUpdatesSinceLastPruneCheck;
+
+@end
+
+@implementation FPersistenceManager
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine cachePolicy:(id<FCachePolicy>)cachePolicy {
+ self = [super init];
+ if (self != nil) {
+ self->_storageEngine = storageEngine;
+ self->_cachePolicy = cachePolicy;
+ self->_trackedQueryManager = [[FTrackedQueryManager alloc] initWithStorageEngine:self.storageEngine
+ clock:[FSystemClock clock]];
+ }
+ return self;
+}
+
+- (void)close {
+ [self.storageEngine close];
+ self.storageEngine = nil;
+ self.trackedQueryManager = nil;
+}
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ [self.storageEngine saveUserOverwrite:node atPath:path writeId:writeId];
+}
+
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ [self.storageEngine saveUserMerge:merge atPath:path writeId:writeId];
+}
+
+- (void)removeUserWrite:(NSUInteger)writeId {
+ [self.storageEngine removeUserWrite:writeId];
+}
+
+- (void)removeAllUserWrites {
+ [self.storageEngine removeAllUserWrites];
+}
+
+- (NSArray *)userWrites {
+ return [self.storageEngine userWrites];
+}
+
+- (FCacheNode *)serverCacheForQuery:(FQuerySpec *)query {
+ NSSet *trackedKeys;
+ BOOL complete;
+ // TODO[offline]: Should we use trackedKeys to find out if this location is a child of a complete query?
+ if ([self.trackedQueryManager isQueryComplete:query]) {
+ complete = YES;
+ FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query];
+ if (!query.loadsAllData && trackedQuery.isComplete) {
+ trackedKeys = [self.storageEngine trackedQueryKeysForQuery:trackedQuery.queryId];
+ } else {
+ trackedKeys = nil;
+ }
+ } else {
+ complete = NO;
+ trackedKeys = [self.trackedQueryManager knownCompleteChildrenAtPath:query.path];
+ }
+
+ id<FNode> node;
+ if (trackedKeys != nil) {
+ node = [self.storageEngine serverCacheForKeys:trackedKeys atPath:query.path];
+ } else {
+ node = [self.storageEngine serverCacheAtPath:query.path];
+ }
+
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:node index:query.index];
+ return [[FCacheNode alloc] initWithIndexedNode:indexedNode isFullyInitialized:complete isFiltered:(trackedKeys != nil)];
+}
+
+- (void)updateServerCacheWithNode:(id<FNode>)node forQuery:(FQuerySpec *)query {
+ BOOL merge = !query.loadsAllData;
+ [self.storageEngine updateServerCache:node atPath:query.path merge:merge];
+ [self setQueryComplete:query];
+ [self doPruneCheckAfterServerUpdate];
+}
+
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path {
+ [self.storageEngine updateServerCacheWithMerge:merge atPath:path];
+ [self doPruneCheckAfterServerUpdate];
+}
+
+- (void)applyUserMerge:(FCompoundWrite *)merge toServerCacheAtPath:(FPath *)path {
+ // TODO[offline]: rework this to be more efficient
+ [merge enumerateWrites:^(FPath *relativePath, id<FNode> node, BOOL *stop) {
+ [self applyUserWrite:node toServerCacheAtPath:[path child:relativePath]];
+ }];
+}
+
+- (void)applyUserWrite:(id<FNode>)write toServerCacheAtPath:(FPath *)path {
+ // This is a hack to guess whether we already cached this because we got a server data update for this
+ // write via an existing active default query. If we didn't, then we'll manually cache this and add a
+ // tracked query to mark it complete and keep it cached.
+ // Unfortunately this is just a guess and it's possible that we *did* get an update (e.g. via a filtered
+ // query) and by overwriting the cache here, we'll actually store an incorrect value (e.g. in the case
+ // that we wrote a ServerValue.TIMESTAMP and the server resolved it to a different value).
+ // TODO[offline]: Consider reworking.
+ if (![self.trackedQueryManager hasActiveDefaultQueryAtPath:path]) {
+ [self.storageEngine updateServerCache:write atPath:path merge:NO];
+ [self.trackedQueryManager ensureCompleteTrackedQueryAtPath:path];
+ }
+}
+
+- (void)setQueryComplete:(FQuerySpec *)query {
+ if (query.loadsAllData) {
+ [self.trackedQueryManager setQueriesCompleteAtPath:query.path];
+ } else {
+ [self.trackedQueryManager setQueryComplete:query];
+ }
+}
+
+- (void)setQueryActive:(FQuerySpec *)spec {
+ [self.trackedQueryManager setQueryActive:spec];
+}
+
+- (void)setQueryInactive:(FQuerySpec *)spec {
+ [self.trackedQueryManager setQueryInactive:spec];
+}
+
+- (void)doPruneCheckAfterServerUpdate {
+ self.serverCacheUpdatesSinceLastPruneCheck++;
+ if ([self.cachePolicy shouldCheckCacheSize:self.serverCacheUpdatesSinceLastPruneCheck]) {
+ FFDebug(@"I-RDB078001", @"Reached prune check threshold. Checking...");
+ NSDate *date = [NSDate date];
+ self.serverCacheUpdatesSinceLastPruneCheck = 0;
+ BOOL canPrune = YES;
+ NSUInteger cacheSize = [self.storageEngine serverCacheEstimatedSizeInBytes];
+ FFDebug(@"I-RDB078002", @"Server cache size: %lu", (unsigned long)cacheSize);
+ while (canPrune && [self.cachePolicy shouldPruneCacheWithSize:cacheSize
+ numberOfTrackedQueries:self.trackedQueryManager.numberOfPrunableQueries]) {
+ FPruneForest *pruneForest = [self.trackedQueryManager pruneOldQueries:self.cachePolicy];
+ if (pruneForest.prunesAnything) {
+ [self.storageEngine pruneCache:pruneForest atPath:[FPath empty]];
+ } else {
+ canPrune = NO;
+ }
+ cacheSize = [self.storageEngine serverCacheEstimatedSizeInBytes];
+ FFDebug(@"I-RDB078003", @"Cache size after pruning: %lu", (unsigned long)cacheSize);
+ }
+ FFDebug(@"I-RDB078004", @"Pruning round took %fms", [date timeIntervalSinceNow]*-1000);
+ }
+}
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQuery:(FQuerySpec *)query {
+ NSAssert(!query.loadsAllData, @"We should only track keys for filtered queries");
+ FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query];
+ NSAssert(trackedQuery.isActive, @"We only expect tracked keys for currently-active queries.");
+ [self.storageEngine setTrackedQueryKeys:keys forQueryId:trackedQuery.queryId];
+}
+
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQuery:(FQuerySpec *)query {
+ NSAssert(!query.loadsAllData, @"We should only track keys for filtered queries");
+ FTrackedQuery *trackedQuery = [self.trackedQueryManager findTrackedQuery:query];
+ NSAssert(trackedQuery.isActive, @"We only expect tracked keys for currently-active queries.");
+ [self.storageEngine updateTrackedQueryKeysWithAddedKeys:added removedKeys:removed forQueryId:trackedQuery.queryId];
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FPruneForest.h b/Firebase/Database/Persistence/FPruneForest.h
new file mode 100644
index 0000000..9e77217
--- /dev/null
+++ b/Firebase/Database/Persistence/FPruneForest.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>
+
+@class FPath;
+
+@interface FPruneForest : NSObject
+
++ (FPruneForest *)empty;
+
+- (BOOL)prunesAnything;
+- (BOOL)shouldPruneUnkeptDescendantsAtPath:(FPath *)path;
+- (BOOL)shouldKeepPath:(FPath *)path;
+- (BOOL)affectsPath:(FPath *)path;
+- (FPruneForest *)child:(NSString *)childKey;
+- (FPruneForest *)childAtPath:(FPath *)childKey;
+- (FPruneForest *)prunePath:(FPath *)path;
+- (FPruneForest *)keepPath:(FPath *)path;
+- (FPruneForest *)keepAll:(NSSet *)children atPath:(FPath *)path;
+- (FPruneForest *)pruneAll:(NSSet *)children atPath:(FPath *)path;
+
+- (void)enumarateKeptNodesUsingBlock:(void (^)(FPath *path))block;
+
+@end
diff --git a/Firebase/Database/Persistence/FPruneForest.m b/Firebase/Database/Persistence/FPruneForest.m
new file mode 100644
index 0000000..3dae6d8
--- /dev/null
+++ b/Firebase/Database/Persistence/FPruneForest.m
@@ -0,0 +1,177 @@
+/*
+ * 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 "FPruneForest.h"
+
+#import "FImmutableTree.h"
+
+@interface FPruneForest ()
+
+@property (nonatomic, strong) FImmutableTree *pruneForest;
+
+@end
+
+@implementation FPruneForest
+
+static BOOL (^kFPrunePredicate)(id) = ^BOOL(NSNumber *pruneValue) {
+ return [pruneValue boolValue];
+};
+
+static BOOL (^kFKeepPredicate)(id) = ^BOOL(NSNumber *pruneValue) {
+ return ![pruneValue boolValue];
+};
+
+
++ (FImmutableTree *)pruneTree {
+ static dispatch_once_t onceToken;
+ static FImmutableTree *pruneTree;
+ dispatch_once(&onceToken, ^{
+ pruneTree = [[FImmutableTree alloc] initWithValue:@YES];
+ });
+ return pruneTree;
+}
+
++ (FImmutableTree *)keepTree {
+ static dispatch_once_t onceToken;
+ static FImmutableTree *keepTree;
+ dispatch_once(&onceToken, ^{
+ keepTree = [[FImmutableTree alloc] initWithValue:@NO];
+ });
+ return keepTree;
+}
+
+- (id) initWithForest:(FImmutableTree *)tree {
+ self = [super init];
+ if (self != nil) {
+ self->_pruneForest = tree;
+ }
+ return self;
+}
+
++ (FPruneForest *)empty {
+ static dispatch_once_t onceToken;
+ static FPruneForest *forest;
+ dispatch_once(&onceToken, ^{
+ forest = [[FPruneForest alloc] initWithForest:[FImmutableTree empty]];
+ });
+ return forest;
+}
+
+- (BOOL)prunesAnything {
+ return [self.pruneForest containsValueMatching:kFPrunePredicate];
+}
+
+- (BOOL)shouldPruneUnkeptDescendantsAtPath:(FPath *)path {
+ NSNumber *shouldPrune = [self.pruneForest leafMostValueOnPath:path];
+ return shouldPrune != nil && [shouldPrune boolValue];
+}
+
+- (BOOL)shouldKeepPath:(FPath *)path {
+ NSNumber *shouldPrune = [self.pruneForest leafMostValueOnPath:path];
+ return shouldPrune != nil && ![shouldPrune boolValue];
+}
+
+- (BOOL)affectsPath:(FPath *)path {
+ return [self.pruneForest rootMostValueOnPath:path] != nil || ![[self.pruneForest subtreeAtPath:path] isEmpty];
+}
+
+- (FPruneForest *)child:(NSString *)childKey {
+ FImmutableTree *childPruneForest = [self.pruneForest.children get:childKey];
+ if (childPruneForest == nil) {
+ if (self.pruneForest.value != nil) {
+ childPruneForest = [self.pruneForest.value boolValue] ? [FPruneForest pruneTree] : [FPruneForest keepTree];
+ } else {
+ childPruneForest = [FImmutableTree empty];
+ }
+ } else {
+ if (childPruneForest.value == nil && self.pruneForest.value != nil) {
+ childPruneForest = [childPruneForest setValue:self.pruneForest.value atPath:[FPath empty]];
+ }
+ }
+ return [[FPruneForest alloc] initWithForest:childPruneForest];
+}
+
+- (FPruneForest *)childAtPath:(FPath *)path {
+ if (path.isEmpty) {
+ return self;
+ } else {
+ return [[self child:path.getFront] childAtPath:[path popFront]];
+ }
+}
+
+- (FPruneForest *)prunePath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't prune path that was kept previously!"];
+ }
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFPrunePredicate]) {
+ // This path will already be pruned
+ return self;
+ } else {
+ FImmutableTree *newPruneForest = [self.pruneForest setTree:[FPruneForest pruneTree] atPath:path];
+ return [[FPruneForest alloc] initWithForest:newPruneForest];
+ }
+}
+
+- (FPruneForest *)keepPath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ // This path will already be kept
+ return self;
+ } else {
+ FImmutableTree *newPruneForest = [self.pruneForest setTree:[FPruneForest keepTree] atPath:path];
+ return [[FPruneForest alloc] initWithForest:newPruneForest];
+ }
+}
+
+- (FPruneForest *)keepAll:(NSSet *)children atPath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ // This path will already be kept
+ return self;
+ } else {
+ return [self setPruneValue:[FPruneForest keepTree] forAll:children atPath:path];
+ }
+}
+
+- (FPruneForest *)pruneAll:(NSSet *)children atPath:(FPath *)path {
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFKeepPredicate]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't prune path that was kept previously!"];
+ }
+ if ([self.pruneForest rootMostValueOnPath:path matching:kFPrunePredicate]) {
+ // This path will already be kept
+ return self;
+ } else {
+ return [self setPruneValue:[FPruneForest pruneTree] forAll:children atPath:path];
+ }
+}
+
+- (FPruneForest *)setPruneValue:(FImmutableTree *)pruneValue forAll:(NSSet *)children atPath:(FPath *)path {
+ FImmutableTree *subtree = [self.pruneForest subtreeAtPath:path];
+ __block FImmutableSortedDictionary *childrenDictionary = subtree.children;
+ [children enumerateObjectsUsingBlock:^(NSString *childKey, BOOL *stop) {
+ childrenDictionary = [childrenDictionary insertKey:childKey withValue:pruneValue];
+ }];
+ FImmutableTree *newSubtree = [[FImmutableTree alloc] initWithValue:subtree.value children:childrenDictionary];
+ return [[FPruneForest alloc] initWithForest:[self.pruneForest setTree:newSubtree atPath:path]];
+}
+
+- (void)enumarateKeptNodesUsingBlock:(void (^)(FPath *))block {
+ [self.pruneForest forEach:^(FPath *path, id value) {
+ if (value != nil && ![value boolValue]) {
+ block(path);
+ }
+ }];
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FStorageEngine.h b/Firebase/Database/Persistence/FStorageEngine.h
new file mode 100644
index 0000000..4f168e7
--- /dev/null
+++ b/Firebase/Database/Persistence/FStorageEngine.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;
+@class FPruneForest;
+@class FPath;
+@class FCompoundWrite;
+@class FQuerySpec;
+@class FTrackedQuery;
+
+@protocol FStorageEngine <NSObject>
+
+- (void)close;
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId;
+- (void)removeUserWrite:(NSUInteger)writeId;
+- (void)removeAllUserWrites;
+- (NSArray *)userWrites;
+
+- (id<FNode>)serverCacheAtPath:(FPath *)path;
+- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path;
+- (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge;
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path;
+- (NSUInteger)serverCacheEstimatedSizeInBytes;
+
+- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)path;
+
+- (NSArray *)loadTrackedQueries;
+- (void)removeTrackedQuery:(NSUInteger)queryId;
+- (void)saveTrackedQuery:(FTrackedQuery *)query;
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId;
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId;
+- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId;
+
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQuery.h b/Firebase/Database/Persistence/FTrackedQuery.h
new file mode 100644
index 0000000..7bc8ef1
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQuery.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 FQuerySpec;
+
+@interface FTrackedQuery : NSObject
+
+@property (nonatomic, readonly) NSUInteger queryId;
+@property (nonatomic, strong, readonly) FQuerySpec *query;
+@property (nonatomic, readonly) NSTimeInterval lastUse;
+@property (nonatomic, readonly) BOOL isComplete;
+@property (nonatomic, readonly) BOOL isActive;
+
+- (id)initWithId:(NSUInteger)queryId query:(FQuerySpec *)query lastUse:(NSTimeInterval)lastUse isActive:(BOOL)isActive;
+- (id)initWithId:(NSUInteger)queryId
+ query:(FQuerySpec *)query
+ lastUse:(NSTimeInterval)lastUse
+ isActive:(BOOL)isActive
+ isComplete:(BOOL)isComplete;
+
+- (FTrackedQuery *)updateLastUse:(NSTimeInterval)lastUse;
+- (FTrackedQuery *)setComplete;
+- (FTrackedQuery *)setActiveState:(BOOL)isActive;
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQuery.m b/Firebase/Database/Persistence/FTrackedQuery.m
new file mode 100644
index 0000000..1720805
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQuery.m
@@ -0,0 +1,102 @@
+/*
+ * 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 "FTrackedQuery.h"
+
+#import "FQuerySpec.h"
+
+@interface FTrackedQuery ()
+
+@property (nonatomic, readwrite) NSUInteger queryId;
+@property (nonatomic, strong, readwrite) FQuerySpec *query;
+@property (nonatomic, readwrite) NSTimeInterval lastUse;
+@property (nonatomic, readwrite) BOOL isComplete;
+@property (nonatomic, readwrite) BOOL isActive;
+
+@end
+
+
+@implementation FTrackedQuery
+
+- (id)initWithId:(NSUInteger)queryId
+ query:(FQuerySpec *)query
+ lastUse:(NSTimeInterval)lastUse
+ isActive:(BOOL)isActive
+ isComplete:(BOOL)isComplete {
+ self = [super init];
+ if (self != nil) {
+ self->_queryId = queryId;
+ self->_query = query;
+ self->_lastUse = lastUse;
+ self->_isComplete = isComplete;
+ self->_isActive = isActive;
+ }
+ return self;
+}
+
+- (id)initWithId:(NSUInteger)queryId query:(FQuerySpec *)query lastUse:(NSTimeInterval)lastUse isActive:(BOOL)isActive {
+ return [self initWithId:queryId query:query lastUse:lastUse isActive:isActive isComplete:NO];
+}
+
+- (FTrackedQuery *)updateLastUse:(NSTimeInterval)lastUse {
+ return [[FTrackedQuery alloc] initWithId:self.queryId
+ query:self.query
+ lastUse:lastUse
+ isActive:self.isActive
+ isComplete:self.isComplete];
+}
+
+- (FTrackedQuery *)setComplete {
+ return [[FTrackedQuery alloc] initWithId:self.queryId
+ query:self.query
+ lastUse:self.lastUse
+ isActive:self.isActive
+ isComplete:YES];
+}
+
+- (FTrackedQuery *)setActiveState:(BOOL)isActive {
+ return [[FTrackedQuery alloc] initWithId:self.queryId
+ query:self.query
+ lastUse:self.lastUse
+ isActive:isActive
+ isComplete:self.isComplete];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FTrackedQuery class]]) {
+ return NO;
+ }
+ FTrackedQuery *other = (FTrackedQuery *)object;
+ if (self.queryId != other.queryId) return NO;
+ if (self.query != other.query && ![self.query isEqual:other.query]) return NO;
+ if (self.lastUse != other.lastUse) return NO;
+ if (self.isComplete != other.isComplete) return NO;
+ if (self.isActive != other.isActive) return NO;
+
+ return YES;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = self.queryId;
+ hash = hash * 31 + self.query.hash;
+ hash = hash * 31 + (self.isActive ? 1 : 0);
+ hash = hash * 31 + (NSUInteger)self.lastUse;
+ hash = hash * 31 + (self.isComplete ? 1 : 0);
+ hash = hash * 31 + (self.isActive ? 1 : 0);
+ return hash;
+}
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQueryManager.h b/Firebase/Database/Persistence/FTrackedQueryManager.h
new file mode 100644
index 0000000..ba2631b
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQueryManager.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 FStorageEngine;
+@protocol FClock;
+@protocol FCachePolicy;
+@class FQuerySpec;
+@class FPath;
+@class FTrackedQuery;
+@class FPruneForest;
+
+@interface FTrackedQueryManager : NSObject
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine clock:(id<FClock>)clock;
+
+- (FTrackedQuery *)findTrackedQuery:(FQuerySpec *)query;
+
+- (BOOL)isQueryComplete:(FQuerySpec *)query;
+
+- (void)removeTrackedQuery:(FQuerySpec *)query;
+- (void)setQueryComplete:(FQuerySpec *)query;
+- (void)setQueriesCompleteAtPath:(FPath *)path;
+- (void)setQueryActive:(FQuerySpec *)query;
+- (void)setQueryInactive:(FQuerySpec *)query;
+
+- (BOOL)hasActiveDefaultQueryAtPath:(FPath *)path;
+- (void)ensureCompleteTrackedQueryAtPath:(FPath *)path;
+
+- (FPruneForest *)pruneOldQueries:(id<FCachePolicy>)cachePolicy;
+- (NSUInteger)numberOfPrunableQueries;
+- (NSSet *)knownCompleteChildrenAtPath:(FPath *)path;
+
+// For testing
+- (void)verifyCache;
+
+@end
diff --git a/Firebase/Database/Persistence/FTrackedQueryManager.m b/Firebase/Database/Persistence/FTrackedQueryManager.m
new file mode 100644
index 0000000..bf9753d
--- /dev/null
+++ b/Firebase/Database/Persistence/FTrackedQueryManager.m
@@ -0,0 +1,321 @@
+/*
+ * 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 "FTrackedQueryManager.h"
+#import "FImmutableTree.h"
+#import "FLevelDBStorageEngine.h"
+#import "FUtilities.h"
+#import "FTrackedQuery.h"
+#import "FPruneForest.h"
+#import "FClock.h"
+#import "FUtilities.h"
+#import "FCachePolicy.h"
+
+@interface FTrackedQueryManager ()
+
+@property (nonatomic, strong) FImmutableTree *trackedQueryTree;
+@property (nonatomic, strong) id<FStorageEngine> storageEngine;
+@property (nonatomic, strong) id<FClock> clock;
+@property (nonatomic) NSUInteger currentQueryId;
+
+@end
+
+@implementation FTrackedQueryManager
+
+- (id)initWithStorageEngine:(id<FStorageEngine>)storageEngine clock:(id<FClock>)clock {
+ self = [super init];
+ if (self != nil) {
+ self->_storageEngine = storageEngine;
+ self->_clock = clock;
+ self->_trackedQueryTree = [FImmutableTree empty];
+
+ NSTimeInterval lastUse = [clock currentTime];
+
+ NSArray *trackedQueries = [self.storageEngine loadTrackedQueries];
+ [trackedQueries enumerateObjectsUsingBlock:^(FTrackedQuery *trackedQuery, NSUInteger idx, BOOL *stop) {
+ self.currentQueryId = MAX(trackedQuery.queryId + 1, self.currentQueryId);
+ if (trackedQuery.isActive) {
+ trackedQuery = [[trackedQuery setActiveState:NO] updateLastUse:lastUse];
+ FFDebug(@"I-RDB081001", @"Setting active query %lu from previous app start inactive", (unsigned long)trackedQuery.queryId);
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ }
+ [self cacheTrackedQuery:trackedQuery];
+ }];
+ }
+ return self;
+}
+
++ (void)assertValidTrackedQuery:(FQuerySpec *)query {
+ NSAssert(!query.loadsAllData || query.isDefault, @"Can't have tracked non-default query that loads all data");
+}
+
++ (FQuerySpec *)normalizeQuery:(FQuerySpec *)query {
+ return query.loadsAllData ? [FQuerySpec defaultQueryAtPath:query.path] : query;
+}
+
+- (FTrackedQuery *)findTrackedQuery:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ NSDictionary *set = [self.trackedQueryTree valueAtPath:query.path];
+ return set[query.params];
+}
+
+- (void)removeTrackedQuery:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+ NSAssert(trackedQuery, @"Tracked query must exist to be removed!");
+
+ [self.storageEngine removeTrackedQuery:trackedQuery.queryId];
+ NSMutableDictionary *trackedQueries = [self.trackedQueryTree valueAtPath:query.path];
+ [trackedQueries removeObjectForKey:query.params];
+}
+
+- (void)setQueryActive:(FQuerySpec *)query {
+ [self setQueryActive:YES forQuery:query];
+}
+
+- (void)setQueryInactive:(FQuerySpec *)query {
+ [self setQueryActive:NO forQuery:query];
+}
+
+- (void)setQueryActive:(BOOL)isActive forQuery:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+
+ // Regardless of whether it's now active or no langer active, we update the lastUse time
+ NSTimeInterval lastUse = [self.clock currentTime];
+ if (trackedQuery != nil) {
+ trackedQuery = [[trackedQuery updateLastUse:lastUse] setActiveState:isActive];
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ } else {
+ NSAssert(isActive, @"If we're setting the query to inactive, we should already be tracking it!");
+ trackedQuery = [[FTrackedQuery alloc] initWithId:self.currentQueryId++
+ query:query
+ lastUse:lastUse
+ isActive:isActive];
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ }
+
+ [self cacheTrackedQuery:trackedQuery];
+}
+
+- (void)setQueryComplete:(FQuerySpec *)query {
+ query = [FTrackedQueryManager normalizeQuery:query];
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+ if (!trackedQuery) {
+ // We might have removed a query and pruned it before we got the complete message from the server...
+ FFWarn(@"I-RDB081002", @"Trying to set a query complete that is not tracked!");
+ } else if (!trackedQuery.isComplete) {
+ trackedQuery = [trackedQuery setComplete];
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ [self cacheTrackedQuery:trackedQuery];
+ } else {
+ // Nothing to do, already marked complete
+ }
+}
+
+- (void)setQueriesCompleteAtPath:(FPath *)path {
+ [[self.trackedQueryTree subtreeAtPath:path] forEach:^(FPath *childPath, NSDictionary *trackedQueries) {
+ [trackedQueries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *parms, FTrackedQuery *trackedQuery, BOOL *stop) {
+ if (!trackedQuery.isComplete) {
+ FTrackedQuery *newTrackedQuery = [trackedQuery setComplete];
+ [self.storageEngine saveTrackedQuery:newTrackedQuery];
+ [self cacheTrackedQuery:newTrackedQuery];
+ }
+ }];
+ }];
+}
+
+- (BOOL)isQueryComplete:(FQuerySpec *)query {
+ if ([self isIncludedInDefaultCompleteQuery:query]) {
+ return YES;
+ } else if (query.loadsAllData) {
+ // We didn't find a default complete query, so must not be complete.
+ return NO;
+ } else {
+ NSDictionary *trackedQueries = [self.trackedQueryTree valueAtPath:query.path];
+ return [trackedQueries[query.params] isComplete];
+ }
+}
+
+- (BOOL)hasActiveDefaultQueryAtPath:(FPath *)path {
+ return [self.trackedQueryTree rootMostValueOnPath:path matching:^BOOL(NSDictionary *trackedQueries) {
+ return [trackedQueries[[FQueryParams defaultInstance]] isActive];
+ }] != nil;
+}
+
+- (void)ensureCompleteTrackedQueryAtPath:(FPath *)path {
+ FQuerySpec *query = [FQuerySpec defaultQueryAtPath:path];
+ if (![self isIncludedInDefaultCompleteQuery:query]) {
+ FTrackedQuery *trackedQuery = [self findTrackedQuery:query];
+ if (trackedQuery == nil) {
+ trackedQuery = [[FTrackedQuery alloc] initWithId:self.currentQueryId++
+ query:query
+ lastUse:[self.clock currentTime]
+ isActive:NO
+ isComplete:YES];
+ } else {
+ NSAssert(!trackedQuery.isComplete, @"This should have been handled above!");
+ trackedQuery = [trackedQuery setComplete];
+ }
+ [self.storageEngine saveTrackedQuery:trackedQuery];
+ [self cacheTrackedQuery:trackedQuery];
+ }
+}
+
+- (BOOL)isIncludedInDefaultCompleteQuery:(FQuerySpec *)query {
+ return [self.trackedQueryTree findRootMostMatchingPath:query.path predicate:^BOOL(NSDictionary *trackedQueries) {
+ return [trackedQueries[[FQueryParams defaultInstance]] isComplete];
+ }] != nil;
+}
+
+- (void)cacheTrackedQuery:(FTrackedQuery *)query {
+ [FTrackedQueryManager assertValidTrackedQuery:query.query];
+ NSMutableDictionary *trackedDict = [self.trackedQueryTree valueAtPath:query.query.path];
+ if (trackedDict == nil) {
+ trackedDict = [NSMutableDictionary dictionary];
+ self.trackedQueryTree = [self.trackedQueryTree setValue:trackedDict atPath:query.query.path];
+ }
+ trackedDict[query.query.params] = query;
+}
+
+- (NSUInteger) numberOfQueriesToPrune:(id<FCachePolicy>)cachePolicy prunableCount:(NSUInteger)numPrunable {
+ NSUInteger numPercent = (NSUInteger)ceilf(numPrunable * [cachePolicy percentOfQueriesToPruneAtOnce]);
+ NSUInteger maxToKeep = [cachePolicy maxNumberOfQueriesToKeep];
+ NSUInteger numMax = (numPrunable > maxToKeep) ? numPrunable - maxToKeep : 0;
+ // Make sure we get below number of max queries to prune
+ return MAX(numMax, numPercent);
+}
+
+- (FPruneForest *)pruneOldQueries:(id<FCachePolicy>)cachePolicy {
+ NSMutableArray *pruneableQueries = [NSMutableArray array];
+ NSMutableArray *unpruneableQueries = [NSMutableArray array];
+ [self.trackedQueryTree forEach:^(FPath *path, NSDictionary *trackedQueries) {
+ [trackedQueries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *params, FTrackedQuery *trackedQuery, BOOL *stop) {
+ if (!trackedQuery.isActive) {
+ [pruneableQueries addObject:trackedQuery];
+ } else {
+ [unpruneableQueries addObject:trackedQuery];
+ }
+ }];
+ }];
+ [pruneableQueries sortUsingComparator:^NSComparisonResult(FTrackedQuery *q1, FTrackedQuery *q2) {
+ if (q1.lastUse < q2.lastUse) {
+ return NSOrderedAscending;
+ } else if (q1.lastUse > q2.lastUse) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ }];
+
+
+ __block FPruneForest *pruneForest = [FPruneForest empty];
+ NSUInteger numToPrune = [self numberOfQueriesToPrune:cachePolicy prunableCount:pruneableQueries.count];
+
+ // TODO: do in transaction
+ for (NSUInteger i = 0; i < numToPrune; i++) {
+ FTrackedQuery *toPrune = pruneableQueries[i];
+ pruneForest = [pruneForest prunePath:toPrune.query.path];
+ [self removeTrackedQuery:toPrune.query];
+ }
+
+ // Keep the rest of the prunable queries
+ for (NSUInteger i = numToPrune; i < pruneableQueries.count; i++) {
+ FTrackedQuery *toKeep = pruneableQueries[i];
+ pruneForest = [pruneForest keepPath:toKeep.query.path];
+ }
+
+ // Also keep unprunable queries
+ [unpruneableQueries enumerateObjectsUsingBlock:^(FTrackedQuery *toKeep, NSUInteger idx, BOOL *stop) {
+ pruneForest = [pruneForest keepPath:toKeep.query.path];
+ }];
+
+ return pruneForest;
+}
+
+- (NSUInteger)numberOfPrunableQueries {
+ __block NSUInteger count = 0;
+ [self.trackedQueryTree forEach:^(FPath *path, NSDictionary *trackedQueries) {
+ [trackedQueries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *params, FTrackedQuery *trackedQuery, BOOL *stop) {
+ if (!trackedQuery.isActive) {
+ count++;
+ }
+ }];
+ }];
+ return count;
+}
+
+- (NSSet *)filteredQueryIdsAtPath:(FPath *)path {
+ NSDictionary *queries = [self.trackedQueryTree valueAtPath:path];
+ if (queries) {
+ NSMutableSet *ids = [NSMutableSet set];
+ [queries enumerateKeysAndObjectsUsingBlock:^(FQueryParams *params, FTrackedQuery *query, BOOL *stop) {
+ if (!query.query.loadsAllData) {
+ [ids addObject:@(query.queryId)];
+ }
+ }];
+ return ids;
+ } else {
+ return [NSSet set];
+ }
+}
+
+- (NSSet *)knownCompleteChildrenAtPath:(FPath *)path {
+ NSAssert(![self isQueryComplete:[FQuerySpec defaultQueryAtPath:path]], @"Path is fully complete");
+
+ NSMutableSet *completeChildren = [NSMutableSet set];
+ // First, get complete children from any queries at this location.
+ NSSet *queryIds = [self filteredQueryIdsAtPath:path];
+ [queryIds enumerateObjectsUsingBlock:^(NSNumber *queryId, BOOL *stop) {
+ NSSet *keys = [self.storageEngine trackedQueryKeysForQuery:[queryId unsignedIntegerValue]];
+ [completeChildren unionSet:keys];
+ }];
+
+ // Second, get any complete default queries immediately below us.
+ [[[self.trackedQueryTree subtreeAtPath:path] children] enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if ([childTree.value[[FQueryParams defaultInstance]] isComplete]) {
+ [completeChildren addObject:childKey];
+ }
+ }];
+
+ return completeChildren;
+}
+
+- (void)verifyCache {
+ NSArray *storedTrackedQueries = [self.storageEngine loadTrackedQueries];
+ NSMutableArray *trackedQueries = [NSMutableArray array];
+
+ [self.trackedQueryTree forEach:^(FPath *path, NSDictionary *queryDict) {
+ [trackedQueries addObjectsFromArray:queryDict.allValues];
+ }];
+ NSComparator comparator = ^NSComparisonResult(FTrackedQuery *q1, FTrackedQuery *q2) {
+ if (q1.queryId < q2.queryId) {
+ return NSOrderedAscending;
+ } else if (q1.queryId > q2.queryId) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ };
+ [trackedQueries sortUsingComparator:comparator];
+ storedTrackedQueries = [storedTrackedQueries sortedArrayUsingComparator:comparator];
+
+ if (![trackedQueries isEqualToArray:storedTrackedQueries]) {
+ [NSException raise:NSInternalInconsistencyException format:@"Tracked queries and queries stored on disk don't match"];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Realtime/FConnection.h b/Firebase/Database/Realtime/FConnection.h
new file mode 100644
index 0000000..ed4879a
--- /dev/null
+++ b/Firebase/Database/Realtime/FConnection.h
@@ -0,0 +1,52 @@
+/*
+ * 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 "FWebSocketConnection.h"
+#import "FTypedefs.h"
+
+@protocol FConnectionDelegate;
+
+@interface FConnection : NSObject <FWebSocketDelegate>
+
+@property (nonatomic, weak) id <FConnectionDelegate> delegate;
+
+- (id)initWith:(FRepoInfo *)aRepoInfo andDispatchQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID;
+
+- (void)open;
+- (void)close;
+- (void)sendRequest:(NSDictionary *)dataMsg sensitive:(BOOL)sensitive;
+
+// FWebSocketDelegate delegate methods
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected;
+
+@end
+
+typedef enum {
+ DISCONNECT_REASON_SERVER_RESET = 0,
+ DISCONNECT_REASON_OTHER = 1
+} FDisconnectReason;
+
+@protocol FConnectionDelegate <NSObject>
+
+- (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;
+
+@end
+
diff --git a/Firebase/Database/Realtime/FConnection.m b/Firebase/Database/Realtime/FConnection.m
new file mode 100644
index 0000000..1550bfc
--- /dev/null
+++ b/Firebase/Database/Realtime/FConnection.m
@@ -0,0 +1,211 @@
+/*
+ * 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 "FConnection.h"
+#import "FConstants.h"
+
+typedef enum {
+ REALTIME_STATE_CONNECTING = 0,
+ REALTIME_STATE_CONNECTED = 1,
+ REALTIME_STATE_DISCONNECTED = 2,
+} FConnectionState;
+
+@interface FConnection () {
+ FConnectionState state;
+}
+
+@property (nonatomic, strong) FWebSocketConnection* conn;
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+
+@end
+
+#pragma mark -
+#pragma mark FConnection implementation
+
+@implementation FConnection
+
+@synthesize delegate;
+@synthesize conn;
+@synthesize repoInfo;
+
+#pragma mark -
+#pragma mark Initializers
+
+- (id)initWith:(FRepoInfo *)aRepoInfo andDispatchQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID{
+ self = [super init];
+ if (self) {
+ state = REALTIME_STATE_CONNECTING;
+ self.repoInfo = aRepoInfo;
+ self.conn = [[FWebSocketConnection alloc] initWith:self.repoInfo andQueue:queue lastSessionID:lastSessionID];
+ self.conn.delegate = self;
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark Public method implementation
+
+- (void)open {
+ FFLog(@"I-RDB082001", @"Calling open in FConnection");
+ [self.conn open];
+}
+
+- (void) closeWithReason:(FDisconnectReason)reason {
+ if (state != REALTIME_STATE_DISCONNECTED) {
+ FFLog(@"I-RDB082002", @"Closing realtime connection.");
+ state = REALTIME_STATE_DISCONNECTED;
+
+ if (self.conn) {
+ FFLog(@"I-RDB082003", @"Calling close again.");
+ [self.conn close];
+ self.conn = nil;
+ }
+
+ [self.delegate onDisconnect:self withReason:reason];
+ }
+}
+
+- (void) close {
+ [self closeWithReason:DISCONNECT_REASON_OTHER];
+}
+
+- (void) sendRequest:(NSDictionary *)dataMsg sensitive:(BOOL)sensitive {
+ // since this came from the persistent connection, wrap it in a data message envelope
+ NSDictionary* msg = @{
+ kFWPRequestType: kFWPRequestTypeData,
+ kFWPRequestDataPayload: dataMsg
+ };
+ [self sendData:msg sensitive:sensitive];
+}
+
+#pragma mark -
+#pragma mark Helpers
+
+
+- (void) sendData:(NSDictionary *)data sensitive:(BOOL)sensitive {
+ if (state != REALTIME_STATE_CONNECTED) {
+ @throw [[NSException alloc] initWithName:@"InvalidConnectionState" reason:@"Tried to send data on an unconnected FConnection" userInfo:nil];
+ } else {
+ if (sensitive) {
+ FFLog(@"I-RDB082004", @"Sending data (contents hidden)");
+ } else {
+ FFLog(@"I-RDB082005", @"Sending: %@", data);
+ }
+ [self.conn send:data];
+ }
+}
+
+#pragma mark -
+#pragma mark FWebSocketConnectinDelegate implementation
+
+// Corresponds to onConnectionLost in JS
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected {
+
+ self.conn = nil;
+ if (!everConnected && state == REALTIME_STATE_CONNECTING) {
+ FFLog(@"I-RDB082006", @"Realtime connection failed.");
+
+ // Since we failed to connect at all, clear any cached entry for this namespace in case the machine went away
+ [self.repoInfo clearInternalHostCache];
+ } else if (state == REALTIME_STATE_CONNECTED) {
+ FFLog(@"I-RDB082007", @"Realtime connection lost.");
+ }
+
+ [self close];
+}
+
+// Corresponds to onMessageReceived in JS
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message {
+ NSString* rawMessageType = [message objectForKey:kFWPAsyncServerEnvelopeType];
+ if(rawMessageType != nil) {
+ if([rawMessageType isEqualToString:kFWPAsyncServerDataMessage]) {
+ [self onDataMessage:[message objectForKey:kFWPAsyncServerEnvelopeData]];
+ }
+ else if ([rawMessageType isEqualToString:kFWPAsyncServerControlMessage]) {
+ [self onControl:[message objectForKey:kFWPAsyncServerEnvelopeData]];
+ }
+ else {
+ FFLog(@"I-RDB082008", @"Unrecognized server packet type: %@", rawMessageType);
+ }
+ }
+ else {
+ FFLog(@"I-RDB082009", @"Unrecognized raw server packet received: %@", message);
+ }
+}
+
+- (void) onDataMessage:(NSDictionary *)message {
+ // we don't do anything with data messages, just kick them up a level
+ FFLog(@"I-RDB082010", @"Got data message: %@", message);
+ [self.delegate onDataMessage:self withMessage:message];
+}
+
+- (void) onControl:(NSDictionary *)message {
+ FFLog(@"I-RDB082011", @"Got control message: %@", message);
+ NSString* type = [message objectForKey:kFWPAsyncServerControlMessageType];
+ if([type isEqualToString:kFWPAsyncServerControlMessageShutdown]) {
+ NSString* reason = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onConnectionShutdownWithReason:reason];
+ }
+ else if ([type isEqualToString:kFWPAsyncServerControlMessageReset]) {
+ NSString* host = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onReset:host];
+ }
+ else if ([type isEqualToString:kFWPAsyncServerHello]) {
+ NSDictionary* handshakeData = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onHandshake:handshakeData];
+ }
+ else {
+ FFLog(@"I-RDB082012", @"Unknown control message returned from server: %@", message);
+ }
+}
+
+- (void) onConnectionShutdownWithReason:(NSString *)reason {
+ FFLog(@"I-RDB082013", @"Connection shutdown command received. Shutting down...");
+
+ [self.delegate onKill:self withReason:reason];
+ [self close];
+}
+
+- (void) onHandshake:(NSDictionary *)handshake {
+ NSNumber* timestamp = [handshake objectForKey:kFWPAsyncServerHelloTimestamp];
+// NSString* version = [handshake objectForKey:kFWPAsyncServerHelloVersion];
+ NSString* host = [handshake objectForKey:kFWPAsyncServerHelloConnectedHost];
+ NSString* sessionID = [handshake objectForKey:kFWPAsyncServerHelloSession];
+
+ self.repoInfo.internalHost = host;
+
+ if (state == REALTIME_STATE_CONNECTING) {
+ [self.conn start];
+ [self onConnection:self.conn readyAtTime:timestamp sessionID:sessionID];
+ }
+}
+
+- (void) onConnection:(FWebSocketConnection *)conn readyAtTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID {
+ FFLog(@"I-RDB082014", @"Realtime connection established");
+ state = REALTIME_STATE_CONNECTED;
+
+ [self.delegate onReady:self atTime:timestamp sessionID:sessionID];
+}
+
+- (void) onReset:(NSString *)host {
+ FFLog(@"I-RDB082015", @"Got a reset; killing connection to: %@; Updating internalHost to: %@", repoInfo.internalHost, host);
+ self.repoInfo.internalHost = host;
+
+ // Explicitly close the connection with SERVER_RESET so calling code knows to reconnect immediately.
+ [self closeWithReason:DISCONNECT_REASON_SERVER_RESET];
+}
+
+@end
diff --git a/Firebase/Database/Realtime/FWebSocketConnection.h b/Firebase/Database/Realtime/FWebSocketConnection.h
new file mode 100644
index 0000000..6a14d47
--- /dev/null
+++ b/Firebase/Database/Realtime/FWebSocketConnection.h
@@ -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 <Foundation/Foundation.h>
+#import "FSRWebSocket.h"
+#import "FUtilities.h"
+
+@protocol FWebSocketDelegate;
+
+@interface FWebSocketConnection : NSObject <FSRWebSocketDelegate>
+
+@property (nonatomic, weak) id <FWebSocketDelegate> delegate;
+
+- (id)initWith:(FRepoInfo *)repoInfo andQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID;
+
+- (void) open;
+- (void) close;
+- (void) start;
+- (void) send:(NSDictionary *)dictionary;
+
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message;
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket;
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error;
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
+
+@end
+
+@protocol FWebSocketDelegate <NSObject>
+
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected;
+
+@end
diff --git a/Firebase/Database/Realtime/FWebSocketConnection.m b/Firebase/Database/Realtime/FWebSocketConnection.m
new file mode 100644
index 0000000..52e2296
--- /dev/null
+++ b/Firebase/Database/Realtime/FWebSocketConnection.m
@@ -0,0 +1,305 @@
+/*
+ * 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.
+ */
+
+// Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+
+#import "FWebSocketConnection.h"
+#import "FConstants.h"
+#import "FIRDatabaseReference.h"
+#import "FStringUtilities.h"
+#import "FIRDatabase_Private.h"
+
+#if TARGET_OS_IPHONE
+#import <UIKit/UIKit.h>
+#endif
+
+@interface FWebSocketConnection () {
+ NSMutableString* frame;
+ BOOL everConnected;
+ BOOL isClosed;
+ NSTimer* keepAlive;
+}
+
+- (void) shutdown;
+- (void) onClosed;
+- (void) closeIfNeverConnected;
+
+@property (nonatomic, strong) FSRWebSocket* webSocket;
+@property (nonatomic, strong) NSNumber* connectionId;
+@property (nonatomic, readwrite) int totalFrames;
+@property (nonatomic, readonly) BOOL buffering;
+@property (nonatomic, readonly) NSString* userAgent;
+@property (nonatomic) dispatch_queue_t dispatchQueue;
+
+- (void)nop:(NSTimer *)timer;
+
+@end
+
+@implementation FWebSocketConnection
+
+@synthesize delegate;
+@synthesize webSocket;
+@synthesize connectionId;
+
+- (id)initWith:(FRepoInfo *)repoInfo andQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID {
+ self = [super init];
+ if (self) {
+ everConnected = NO;
+ isClosed = NO;
+ self.connectionId = [FUtilities LUIDGenerator];
+ self.totalFrames = 0;
+ self.dispatchQueue = queue;
+ frame = nil;
+
+ NSString* connectionUrl = [repoInfo connectionURLWithLastSessionID:lastSessionID];
+ NSString* ua = [self userAgent];
+ FFLog(@"I-RDB083001", @"(wsc:%@) Connecting to: %@ as %@", self.connectionId, connectionUrl, ua);
+
+ NSURLRequest* req = [[NSURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:connectionUrl]];
+ self.webSocket = [[FSRWebSocket alloc] initWithURLRequest:req queue:queue andUserAgent:ua];
+ [self.webSocket setDelegateDispatchQueue:queue];
+ self.webSocket.delegate = self;
+ }
+ return self;
+}
+
+- (NSString *) userAgent {
+ NSString* systemVersion;
+ NSString* deviceName;
+ BOOL hasUiDeviceClass = NO;
+
+ // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+ #if TARGET_OS_IPHONE
+ Class uiDeviceClass = NSClassFromString(@"UIDevice");
+ if (uiDeviceClass) {
+ systemVersion = [uiDeviceClass currentDevice].systemVersion;
+ deviceName = [uiDeviceClass currentDevice].model;
+ hasUiDeviceClass = YES;
+ }
+ #endif
+
+ if (!hasUiDeviceClass) {
+ NSDictionary *systemVersionDictionary = [NSDictionary dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"];
+ systemVersion = [systemVersionDictionary objectForKey:@"ProductVersion"];
+ deviceName = [systemVersionDictionary objectForKey:@"ProductName"];
+ }
+
+ NSString* bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
+
+ // Sanitize '/'s in deviceName and bundleIdentifier for stats
+ deviceName = [FStringUtilities sanitizedForUserAgent:deviceName];
+ bundleIdentifier = [FStringUtilities sanitizedForUserAgent:bundleIdentifier];
+
+ // Firebase/5/<semver>_<build date>_<git hash>/<os version>/{device model / os (Mac OS X, iPhone, etc.}_<bundle id>
+ NSString* ua = [NSString stringWithFormat:@"Firebase/%@/%@/%@/%@_%@", kWebsocketProtocolVersion, [FIRDatabase buildVersion], systemVersion, deviceName, bundleIdentifier];
+ return ua;
+}
+
+- (BOOL) buffering {
+ return frame != nil;
+}
+
+#pragma mark -
+#pragma mark Public FWebSocketConnection methods
+
+- (void) open {
+ FFLog(@"I-RDB083002", @"(wsc:%@) FWebSocketConnection open.", self.connectionId);
+ assert(delegate);
+ everConnected = NO;
+ // TODO Assert url
+ [self.webSocket open];
+ dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, kWebsocketConnectTimeout * NSEC_PER_SEC);
+ dispatch_after(when, self.dispatchQueue, ^{
+ [self closeIfNeverConnected];
+ });
+}
+
+- (void) close {
+ FFLog(@"I-RDB083003", @"(wsc:%@) FWebSocketConnection is being closed.", self.connectionId);
+ isClosed = YES;
+ [self.webSocket close];
+}
+
+- (void) start {
+ // Start is a no-op for websockets.
+}
+
+- (void) send:(NSDictionary *)dictionary {
+
+ [self resetKeepAlive];
+
+ NSData* jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
+ options:kNilOptions error:nil];
+
+ NSString* data = [[NSString alloc] initWithData:jsonData
+ encoding:NSUTF8StringEncoding];
+
+ NSArray* dataSegs = [FUtilities splitString:data intoMaxSize:kWebsocketMaxFrameSize];
+
+ // First send the header so the server knows how many segments are forthcoming
+ if (dataSegs.count > 1) {
+ [self.webSocket send:[NSString stringWithFormat:@"%u", (unsigned int)dataSegs.count]];
+ }
+
+ // Then, actually send the segments.
+ for(NSString * segment in dataSegs) {
+ [self.webSocket send:segment];
+ }
+}
+
+- (void) nop:(NSTimer *)timer {
+ if(self.webSocket) {
+ FFLog(@"I-RDB083004", @"(wsc:%@) nop", self.connectionId);
+ [self.webSocket send:@"0"];
+ }
+ else {
+ FFLog(@"I-RDB083005", @"(wsc:%@) No more websocket; invalidating nop timer.", self.connectionId);
+ [timer invalidate];
+ }
+}
+
+- (void) handleNewFrameCount:(int) numFrames {
+ self.totalFrames = numFrames;
+ frame = [[NSMutableString alloc] initWithString:@""];
+ FFLog(@"I-RDB083006", @"(wsc:%@) handleNewFrameCount: %d", self.connectionId, self.totalFrames);
+}
+
+- (NSString *) extractFrameCount:(NSString *) message {
+ if ([message length] <= 4) {
+ int frameCount = [message intValue];
+ if (frameCount > 0) {
+ [self handleNewFrameCount:frameCount];
+ return nil;
+ }
+ }
+ [self handleNewFrameCount:1];
+ return message;
+}
+
+- (void) appendFrame:(NSString *) message {
+ [frame appendString:message];
+ self.totalFrames = self.totalFrames - 1;
+
+ if (self.totalFrames == 0) {
+ // Call delegate and pass an immutable version of the frame
+ NSDictionary* json = [NSJSONSerialization JSONObjectWithData:[frame dataUsingEncoding:NSUTF8StringEncoding]
+ options:kNilOptions
+ error:nil];
+ frame = nil;
+ FFLog(@"I-RDB083007", @"(wsc:%@) handleIncomingFrame sending complete frame: %d", self.connectionId, self.totalFrames);
+
+ @autoreleasepool {
+ [self.delegate onMessage:self withMessage:json];
+ }
+ }
+}
+
+- (void) handleIncomingFrame:(NSString *) message {
+ [self resetKeepAlive];
+ if (self.buffering) {
+ [self appendFrame:message];
+ } else {
+ NSString *remaining = [self extractFrameCount:message];
+ if (remaining) {
+ [self appendFrame:remaining];
+ }
+ }
+}
+
+#pragma mark -
+#pragma mark SRWebSocketDelegate implementation
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message
+{
+ [self handleIncomingFrame:message];
+}
+
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket
+{
+ FFLog(@"I-RDB083008", @"(wsc:%@) webSocketDidOpen", self.connectionId);
+
+ everConnected = YES;
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ self->keepAlive = [NSTimer scheduledTimerWithTimeInterval:kWebsocketKeepaliveInterval
+ target:self
+ selector:@selector(nop:)
+ userInfo:nil
+ repeats:YES];
+ FFLog(@"I-RDB083009", @"(wsc:%@) nop timer kicked off", self.connectionId);
+ });
+}
+
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error
+{
+ FFLog(@"I-RDB083010", @"(wsc:%@) didFailWithError didFailWithError: %@", self.connectionId, [error description]);
+ [self onClosed];
+}
+
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
+{
+ FFLog(@"I-RDB083011", @"(wsc:%@) didCloseWithCode: %ld %@", self.connectionId, (long)code, reason);
+ [self onClosed];
+}
+
+#pragma mark -
+#pragma mark Private methods
+
+/**
+ * Note that the close / onClosed / shutdown cycle here is a little different from the javascript client.
+ * In order to properly handle deallocation, no close-related action is taken at a higher level until we
+ * have received notification from the websocket itself that it is closed. Otherwise, we end up deallocating
+ * this class and the FConnection class before the websocket has a change to call some of its delegate methods.
+ * So, since close is the external close handler, we just set a flag saying not to call our own delegate method
+ * and close the websocket. That will trigger a callback into this class that can then do things like clean up
+ * the keepalive timer.
+ */
+
+- (void) closeIfNeverConnected {
+ if (!everConnected) {
+ FFLog(@"I-RDB083012", @"(wsc:%@) Websocket timed out on connect", self.connectionId);
+ [self.webSocket close];
+ }
+}
+
+- (void) shutdown {
+ isClosed = YES;
+
+ // Call delegate methods
+ [self.delegate onDisconnect:self wasEverConnected:everConnected];
+
+}
+
+- (void) onClosed {
+ if (!isClosed) {
+ FFLog(@"I-RDB083013", @"Websocket is closing itself");
+ [self shutdown];
+ }
+ self.webSocket = nil;
+ if (keepAlive.isValid) {
+ [keepAlive invalidate];
+ }
+}
+
+- (void) resetKeepAlive {
+ NSDate* newTime = [NSDate dateWithTimeIntervalSinceNow:kWebsocketKeepaliveInterval];
+ // Calling setFireDate is actually kinda' expensive, so wait at least 5 seconds before updating it.
+ if ([newTime timeIntervalSinceDate:keepAlive.fireDate] > 5) {
+ FFLog(@"I-RDB083014", @"(wsc:%@) resetting keepalive, to %@ ; old: %@", self.connectionId, newTime, [keepAlive fireDate]);
+ [keepAlive setFireDate:newTime];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FChildrenNode.h b/Firebase/Database/Snapshot/FChildrenNode.h
new file mode 100644
index 0000000..9eebdae
--- /dev/null
+++ b/Firebase/Database/Snapshot/FChildrenNode.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"
+#import "FTypedefs.h"
+#import "FTypedefs_Private.h"
+#import "FImmutableSortedDictionary.h"
+
+@class FNamedNode;
+
+@interface FChildrenNode : NSObject <FNode>
+
+- (id)initWithChildren:(FImmutableSortedDictionary *)someChildren;
+- (id)initWithPriority:(id<FNode>)aPriority children:(FImmutableSortedDictionary *)someChildren;
+
+// FChildrenNode specific methods
+
+- (void) enumerateChildrenAndPriorityUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block;
+
+- (FNamedNode *) firstChild;
+- (FNamedNode *) lastChild;
+
+@property (nonatomic, strong) FImmutableSortedDictionary* children;
+@property (nonatomic, strong) id<FNode> priorityNode;
+
+@end
diff --git a/Firebase/Database/Snapshot/FChildrenNode.m b/Firebase/Database/Snapshot/FChildrenNode.m
new file mode 100644
index 0000000..b5598ad
--- /dev/null
+++ b/Firebase/Database/Snapshot/FChildrenNode.m
@@ -0,0 +1,385 @@
+/*
+ * 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 "FChildrenNode.h"
+#import "FEmptyNode.h"
+#import "FConstants.h"
+#import "FStringUtilities.h"
+#import "FUtilities.h"
+#import "FNamedNode.h"
+#import "FMaxNode.h"
+#import "FTransformedEnumerator.h"
+#import "FSnapshotUtilities.h"
+#import "FTransformedEnumerator.h"
+#import "FPriorityIndex.h"
+#import "FUtilities.h"
+
+@interface FChildrenNode ()
+@property (nonatomic, strong) NSString *lazyHash;
+@end
+
+@implementation FChildrenNode
+
+// Note: The only reason we allow nil priority is to for EmptyNode, since we can't use
+// EmptyNode as the priority of EmptyNode. We might want to consider making EmptyNode its own
+// class instead of an empty ChildrenNode.
+
+- (id)init {
+ return [self initWithPriority:nil children:[FImmutableSortedDictionary dictionaryWithComparator:[FUtilities keyComparator]]];
+}
+
+- (id)initWithChildren:(FImmutableSortedDictionary *)someChildren {
+ return [self initWithPriority:nil children:someChildren];
+}
+
+- (id)initWithPriority:(id<FNode>)aPriority children:(FImmutableSortedDictionary *)someChildren {
+ if (someChildren.isEmpty && aPriority != nil && ![aPriority isEmpty]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't create empty node with priority!"];
+ }
+ self = [super init];
+ if(self) {
+ self.children = someChildren;
+ self.priorityNode = aPriority;
+ }
+ return self;
+}
+
+- (NSString *) description {
+ return [[self valForExport:YES] description];
+}
+
+#pragma mark -
+#pragma mark FNode methods
+
+
+- (BOOL) isLeafNode {
+ return NO;
+}
+
+- (id<FNode>) getPriority {
+ if (self.priorityNode) {
+ return self.priorityNode;
+ } else {
+ return [FEmptyNode emptyNode];
+ }
+
+}
+
+- (id<FNode>) updatePriority:(id<FNode>)aPriority {
+ if ([self.children isEmpty]) {
+ return [FEmptyNode emptyNode];
+ } else {
+ return [[FChildrenNode alloc] initWithPriority:aPriority children:self.children];
+ }
+}
+
+- (id<FNode>) getImmediateChild:(NSString *) childName {
+ if ([childName isEqualToString:@".priority"]) {
+ return [self getPriority];
+ } else {
+ id <FNode> child = [self.children objectForKey:childName];
+ return (child == nil) ? [FEmptyNode emptyNode] : child;
+ }
+}
+
+- (id<FNode>) getChild:(FPath *)path {
+ NSString* front = [path getFront];
+ if(front == nil) {
+ return self;
+ }
+ else {
+ return [[self getImmediateChild:front] getChild:[path popFront]];
+ }
+}
+
+- (BOOL)hasChild:(NSString *)childName {
+ return ![self getImmediateChild:childName].isEmpty;
+}
+
+
+- (id<FNode>) updateImmediateChild:(NSString *)childName withNewChild:(id<FNode>)newChildNode {
+ NSAssert(newChildNode != nil, @"Should always be passing nodes.");
+
+ if ([childName isEqualToString:@".priority"]) {
+ return [self updatePriority:newChildNode];
+ } else {
+ FImmutableSortedDictionary *newChildren;
+ if (newChildNode.isEmpty) {
+ newChildren = [self.children removeObjectForKey:childName];
+ } else {
+ newChildren = [self.children setObject:newChildNode forKey:childName];
+ }
+ if (newChildren.isEmpty) {
+ return [FEmptyNode emptyNode];
+ } else {
+ return [[FChildrenNode alloc] initWithPriority:self.getPriority children:newChildren];
+ }
+ }
+}
+
+- (id<FNode>) updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode {
+ NSString* front = [path getFront];
+ if(front == nil) {
+ return newChildNode;
+ } else {
+ NSAssert(![front isEqualToString:@".priority"] || path.length == 1, @".priority must be the last token in a path.");
+ id<FNode> newImmediateChild = [[self getImmediateChild:front] updateChild:[path popFront] withNewChild:newChildNode];
+ return [self updateImmediateChild:front withNewChild:newImmediateChild];
+ }
+}
+
+- (BOOL) isEmpty {
+ return [self.children isEmpty];
+}
+
+- (int) numChildren {
+ return [self.children count];
+}
+
+- (id) val {
+ return [self valForExport:NO];
+}
+
+- (id) valForExport:(BOOL)exp {
+ if([self isEmpty]) {
+ return [NSNull null];
+ }
+
+ __block int numKeys = 0;
+ __block NSInteger maxKey = 0;
+ __block BOOL allIntegerKeys = YES;
+
+ NSMutableDictionary* obj = [[NSMutableDictionary alloc] initWithCapacity:[self.children count]];
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> childNode, BOOL *stop) {
+ [obj setObject:[childNode valForExport:exp] forKey:key];
+
+ numKeys++;
+
+ // If we already found a string key, don't bother with any of this
+ if (!allIntegerKeys) {
+ return;
+ }
+
+ // Treat leading zeroes that are not exactly "0" as strings
+ NSString* firstChar = [key substringWithRange:NSMakeRange(0, 1)];
+ if ([firstChar isEqualToString:@"0"] && [key length] > 1) {
+ allIntegerKeys = NO;
+ } else {
+ NSNumber *keyAsNum = [FUtilities intForString:key];
+ if (keyAsNum != nil) {
+ NSInteger keyAsInt = [keyAsNum integerValue];
+ if (keyAsInt > maxKey) {
+ maxKey = keyAsInt;
+ }
+ } else {
+ allIntegerKeys = NO;
+ }
+ }
+ }];
+
+ if (!exp && allIntegerKeys && maxKey < 2 * numKeys) {
+ // convert to an array
+ NSMutableArray* array = [[NSMutableArray alloc] initWithCapacity:maxKey + 1];
+ for (int i = 0; i <= maxKey; ++i) {
+ NSString* keyString = [NSString stringWithFormat:@"%i", i];
+ id child = obj[keyString];
+ if (child != nil) {
+ [array addObject:child];
+ } else {
+ [array addObject:[NSNull null]];
+ }
+ }
+ return array;
+ } else {
+
+ if(exp && [self getPriority] != nil && !self.getPriority.isEmpty) {
+ obj[kPayloadPriority] = [self.getPriority val];
+ }
+
+ return obj;
+ }
+}
+
+- (NSString *) dataHash {
+ if (self.lazyHash == nil) {
+ NSMutableString *toHash = [[NSMutableString alloc] init];
+
+ if (!self.getPriority.isEmpty) {
+ [toHash appendString:@"priority:"];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:(FLeafNode *)self.getPriority
+ toString:toHash
+ hashVersion:FDataHashVersionV1];
+ [toHash appendString:@":"];
+ }
+
+ __block BOOL sawPriority = NO;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ sawPriority = sawPriority || [[node getPriority] isEmpty];
+ *stop = sawPriority;
+ }];
+ if (sawPriority) {
+ NSMutableArray *array = [NSMutableArray array];
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FNamedNode *namedNode = [[FNamedNode alloc] initWithName:key andNode:node];
+ [array addObject:namedNode];
+ }];
+ [array sortUsingComparator:^NSComparisonResult(FNamedNode *namedNode1, FNamedNode *namedNode2) {
+ return [[FPriorityIndex priorityIndex] compareNamedNode:namedNode1 toNamedNode:namedNode2];
+ }];
+ [array enumerateObjectsUsingBlock:^(FNamedNode *namedNode, NSUInteger idx, BOOL *stop) {
+ NSString *childHash = [namedNode.node dataHash];
+ if (![childHash isEqualToString:@""]) {
+ [toHash appendFormat:@":%@:%@", namedNode.name, childHash];
+ }
+ }];
+ } else {
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ NSString *childHash = [node dataHash];
+ if (![childHash isEqualToString:@""]) {
+ [toHash appendFormat:@":%@:%@", key, childHash];
+ }
+ }];
+ }
+ self.lazyHash = [toHash isEqualToString:@""] ? @"" : [FStringUtilities base64EncodedSha1:toHash];
+ }
+ return self.lazyHash;
+}
+
+- (NSComparisonResult)compare:(id <FNode>)other {
+ // children nodes come last, unless this is actually an empty node, then we come first.
+ if (self.isEmpty) {
+ if (other.isEmpty) {
+ return NSOrderedSame;
+ } else {
+ return NSOrderedAscending;
+ }
+ } else if (other.isLeafNode || other.isEmpty) {
+ return NSOrderedDescending;
+ } else if (other == [FMaxNode maxNode]) {
+ return NSOrderedAscending;
+ } else {
+ // Must be another node with children.
+ return NSOrderedSame;
+ }
+}
+
+- (BOOL)isEqual:(id <FNode>)other {
+ if (other == self) {
+ return YES;
+ } else if (other == nil) {
+ return NO;
+ } else if (other.isLeafNode) {
+ return NO;
+ } else if (self.isEmpty && [other isEmpty]) {
+ // Empty nodes do not have priority
+ return YES;
+ } else {
+ FChildrenNode *otherChildrenNode = other;
+ if (![self.getPriority isEqual:other.getPriority]) {
+ return NO;
+ } else if (self.children.count == otherChildrenNode.children.count) {
+ __block BOOL equal = YES;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ id<FNode> child = [otherChildrenNode getImmediateChild:key];
+ if (![child isEqual:node]) {
+ equal = NO;
+ *stop = YES;
+ }
+ }];
+ return equal;
+ } else {
+ return NO;
+ }
+ }
+}
+
+- (NSUInteger)hash {
+ __block NSUInteger hashCode = 0;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ hashCode = 31 * hashCode + key.hash;
+ hashCode = 17 * hashCode + node.hash;
+ }];
+ return 17 * hashCode + self.priorityNode.hash;
+}
+
+- (void) enumerateChildrenAndPriorityUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ if ([self.getPriority isEmpty]) {
+ [self enumerateChildrenUsingBlock:block];
+ } else {
+ __block BOOL passedPriorityKey = NO;
+ [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ if (!passedPriorityKey && [FUtilities compareKey:key toKey:@".priority"] == NSOrderedDescending) {
+ passedPriorityKey = YES;
+ BOOL stopAfterPriority = NO;
+ block(@".priority", [self getPriority], &stopAfterPriority);
+ if (stopAfterPriority) return;
+ }
+ block(key, node, stop);
+ }];
+ }
+}
+
+- (void) enumerateChildrenUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ [self.children enumerateKeysAndObjectsUsingBlock:block];
+}
+
+- (void) enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ [self.children enumerateKeysAndObjectsReverse:reverse usingBlock:block];
+}
+
+- (NSEnumerator *)childEnumerator
+{
+ return [[FTransformedEnumerator alloc] initWithEnumerator:self.children.keyEnumerator andTransform:^id(NSString *key) {
+ return [FNamedNode nodeWithName:key node:[self getImmediateChild:key]];
+ }];
+}
+
+- (NSString *) predecessorChildKey:(NSString *)childKey
+{
+ return [self.children getPredecessorKey:childKey];
+}
+
+#pragma mark -
+#pragma mark FChildrenNode specific methods
+
+- (id) childrenGetter:(id)key {
+ return [self.children objectForKey:key];
+}
+
+- (FNamedNode *)firstChild
+{
+ NSString *childKey = self.children.minKey;
+ if (childKey) {
+ return [[FNamedNode alloc] initWithName:childKey andNode:[self getImmediateChild:childKey]];
+ } else {
+ return nil;
+ }
+}
+
+- (FNamedNode *)lastChild
+{
+ NSString *childKey = self.children.maxKey;
+ if (childKey) {
+ return [[FNamedNode alloc] initWithName:childKey andNode:[self getImmediateChild:childKey]];
+ } else {
+ return nil;
+ }
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FCompoundWrite.h b/Firebase/Database/Snapshot/FCompoundWrite.h
new file mode 100644
index 0000000..c67cfc7
--- /dev/null
+++ b/Firebase/Database/Snapshot/FCompoundWrite.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 FImmutableTree;
+@protocol FNode;
+@class FPath;
+
+/**
+* This class holds a collection of writes that can be applied to nodes in unison. It abstracts away the logic with
+* dealing with priority writes and multiple nested writes. At any given path, there is only allowed to be one write
+* modifying that path. Any write to an existing path or shadowing an existing path will modify that existing write to
+* reflect the write added.
+*/
+@interface FCompoundWrite : NSObject
+
+- (id) initWithWriteTree:(FImmutableTree *)tree;
+
+/**
+ * Creates a compound write with NSDictionary from path string to object
+ */
++ (FCompoundWrite *) compoundWriteWithValueDictionary:(NSDictionary *)dictionary;
+/**
+ * Creates a compound write with NSDictionary from path string to node
+ */
++ (FCompoundWrite *) compoundWriteWithNodeDictionary:(NSDictionary *)dictionary;
+
++ (FCompoundWrite *) emptyWrite;
+
+- (FCompoundWrite *) addWrite:(id<FNode>)node atPath:(FPath *)path;
+- (FCompoundWrite *) addWrite:(id<FNode>)node atKey:(NSString *)key;
+- (FCompoundWrite *) addCompoundWrite:(FCompoundWrite *)node atPath:(FPath *)path;
+- (FCompoundWrite *) removeWriteAtPath:(FPath *)path;
+- (id<FNode>)rootWrite;
+- (BOOL) hasCompleteWriteAtPath:(FPath *)path;
+- (id<FNode>) completeNodeAtPath:(FPath *)path;
+- (NSArray *) completeChildren;
+- (NSDictionary *)childCompoundWrites;
+- (FCompoundWrite *) childCompoundWriteAtPath:(FPath *)path;
+- (id<FNode>) applyToNode:(id<FNode>)node;
+- (void)enumerateWrites:(void (^)(FPath *path, id<FNode>node, BOOL *stop))block;
+
+- (NSDictionary *)valForExport:(BOOL)exportFormat;
+
+- (BOOL) isEmpty;
+
+@end
diff --git a/Firebase/Database/Snapshot/FCompoundWrite.m b/Firebase/Database/Snapshot/FCompoundWrite.m
new file mode 100644
index 0000000..8887095
--- /dev/null
+++ b/Firebase/Database/Snapshot/FCompoundWrite.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 "FCompoundWrite.h"
+#import "FImmutableTree.h"
+#import "FNode.h"
+#import "FPath.h"
+#import "FNamedNode.h"
+#import "FSnapshotUtilities.h"
+
+@interface FCompoundWrite ()
+@property (nonatomic, strong) FImmutableTree *writeTree;
+@end
+
+@implementation FCompoundWrite
+
+- (id) initWithWriteTree:(FImmutableTree *)tree {
+ self = [super init];
+ if (self) {
+ self.writeTree = tree;
+ }
+ return self;
+}
+
++ (FCompoundWrite *)compoundWriteWithValueDictionary:(NSDictionary *)dictionary {
+ __block FImmutableTree *writeTree = [FImmutableTree empty];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *pathString, id value, BOOL *stop) {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value];
+ FImmutableTree *tree = [[FImmutableTree alloc] initWithValue:node];
+ writeTree = [writeTree setTree:tree atPath:[[FPath alloc] initWith:pathString]];
+ }];
+ return [[FCompoundWrite alloc] initWithWriteTree:writeTree];
+}
+
++ (FCompoundWrite *)compoundWriteWithNodeDictionary:(NSDictionary *)dictionary {
+ __block FImmutableTree *writeTree = [FImmutableTree empty];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *pathString, id node, BOOL *stop) {
+ FImmutableTree *tree = [[FImmutableTree alloc] initWithValue:node];
+ writeTree = [writeTree setTree:tree atPath:[[FPath alloc] initWith:pathString]];
+ }];
+ return [[FCompoundWrite alloc] initWithWriteTree:writeTree];
+}
+
++ (FCompoundWrite *) emptyWrite {
+ static dispatch_once_t pred = 0;
+ static FCompoundWrite *empty = nil;
+ dispatch_once(&pred, ^{
+ empty = [[FCompoundWrite alloc] initWithWriteTree:[[FImmutableTree alloc] initWithValue:nil]];
+ });
+ return empty;
+}
+
+- (FCompoundWrite *) addWrite:(id<FNode>)node atPath:(FPath *)path {
+ if (path.isEmpty) {
+ return [[FCompoundWrite alloc] initWithWriteTree:[[FImmutableTree alloc] initWithValue:node]];
+ } else {
+ FTuplePathValue *rootMost = [self.writeTree findRootMostValueAndPath:path];
+ if (rootMost != nil) {
+ FPath *relativePath = [FPath relativePathFrom:rootMost.path to:path];
+ id<FNode> value = [rootMost.value updateChild:relativePath withNewChild:node];
+ return [[FCompoundWrite alloc] initWithWriteTree:[self.writeTree setValue:value atPath:rootMost.path]];
+ } else {
+ FImmutableTree *subtree = [[FImmutableTree alloc] initWithValue:node];
+ FImmutableTree *newWriteTree = [self.writeTree setTree:subtree atPath:path];
+ return [[FCompoundWrite alloc] initWithWriteTree:newWriteTree];
+ }
+ }
+}
+
+- (FCompoundWrite *) addWrite:(id<FNode>)node atKey:(NSString *)key {
+ return [self addWrite:node atPath:[[FPath alloc] initWith:key]];
+}
+
+- (FCompoundWrite *) addCompoundWrite:(FCompoundWrite *)compoundWrite atPath:(FPath *)path {
+ __block FCompoundWrite *newWrite = self;
+ [compoundWrite.writeTree forEach:^(FPath *childPath, id<FNode> value) {
+ newWrite = [newWrite addWrite:value atPath:[path child:childPath]];
+ }];
+ return newWrite;
+}
+
+/**
+* Will remove a write at the given path and deeper paths. This will <em>not</em> modify a write at a higher location,
+* which must be removed by calling this method with that path.
+* @param path The path at which a write and all deeper writes should be removed.
+* @return The new FWriteCompound with the removed path.
+*/
+- (FCompoundWrite *) removeWriteAtPath:(FPath *)path {
+ if (path.isEmpty) {
+ return [FCompoundWrite emptyWrite];
+ } else {
+ FImmutableTree *newWriteTree = [self.writeTree setTree:[FImmutableTree empty] atPath:path];
+ return [[FCompoundWrite alloc] initWithWriteTree:newWriteTree];
+ }
+}
+
+/**
+* Returns whether this FCompoundWrite will fully overwrite a node at a given location and can therefore be considered
+* "complete".
+* @param path The path to check for
+* @return Whether there is a complete write at that path.
+*/
+- (BOOL) hasCompleteWriteAtPath:(FPath *)path {
+ return [self completeNodeAtPath:path] != nil;
+}
+
+/**
+* Returns a node for a path if and only if the node is a "complete" overwrite at that path. This will not aggregate
+* writes from depeer paths, but will return child nodes from a more shallow path.
+* @param path The path to get a complete write
+* @return The node if complete at that path, or nil otherwise.
+*/
+- (id<FNode>) completeNodeAtPath:(FPath *)path {
+ FTuplePathValue *rootMost = [self.writeTree findRootMostValueAndPath:path];
+ if (rootMost != nil) {
+ FPath *relativePath = [FPath relativePathFrom:rootMost.path to:path];
+ return [rootMost.value getChild:relativePath];
+ } else {
+ return nil;
+ }
+}
+
+// TODO: change into traversal method...
+- (NSArray *) completeChildren {
+ NSMutableArray *children = [[NSMutableArray alloc] init];
+ if (self.writeTree.value != nil) {
+ id<FNode> node = self.writeTree.value;
+ [node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ [children addObject:[[FNamedNode alloc] initWithName:key andNode:node]];
+ }];
+ } else {
+ [self.writeTree.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if (childTree.value != nil) {
+ [children addObject:[[FNamedNode alloc] initWithName:childKey andNode:childTree.value]];
+ }
+ }];
+ }
+ return children;
+}
+
+
+// TODO: change into enumarate method
+- (NSDictionary *)childCompoundWrites {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ [self.writeTree.children enumerateKeysAndObjectsUsingBlock:^(NSString *key, FImmutableTree *childWrite, BOOL *stop) {
+ dict[key] = [[FCompoundWrite alloc] initWithWriteTree:childWrite];
+ }];
+ return dict;
+}
+
+- (FCompoundWrite *) childCompoundWriteAtPath:(FPath *)path {
+ if (path.isEmpty) {
+ return self;
+ } else {
+ id<FNode> shadowingNode = [self completeNodeAtPath:path];
+ if (shadowingNode != nil) {
+ return [[FCompoundWrite alloc] initWithWriteTree:[[FImmutableTree alloc] initWithValue:shadowingNode]];
+ } else {
+ return [[FCompoundWrite alloc] initWithWriteTree:[self.writeTree subtreeAtPath:path]];
+ }
+ }
+}
+
+- (id<FNode>) applySubtreeWrite:(FImmutableTree *)subtreeWrite atPath:(FPath *)relativePath toNode:(id<FNode>)node {
+ if (subtreeWrite.value != nil) {
+ // Since a write there is always a leaf, we're done here.
+ return [node updateChild:relativePath withNewChild:subtreeWrite.value];
+ } else {
+ __block id<FNode> priorityWrite = nil;
+ __block id<FNode> blockNode = node;
+ [subtreeWrite.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) {
+ if ([childKey isEqualToString:@".priority"]) {
+ // Apply priorities at the end so we don't update priorities for either empty nodes or forget to apply
+ // priorities to empty nodes that are later filled.
+ NSAssert(childTree.value != nil, @"Priority writes must always be leaf nodes");
+ priorityWrite = childTree.value;
+ } else {
+ blockNode = [self applySubtreeWrite:childTree atPath:[relativePath childFromString:childKey] toNode:blockNode];
+ }
+ }];
+ // If there was a priority write, we only apply it if the node is not empty
+ if (![blockNode getChild:relativePath].isEmpty && priorityWrite != nil) {
+ blockNode = [blockNode updateChild:[relativePath childFromString:@".priority"] withNewChild:priorityWrite];
+ }
+ return blockNode;
+ }
+}
+
+- (void)enumerateWrites:(void (^)(FPath *, id<FNode>, BOOL *))block {
+ __block BOOL stop = NO;
+ // TODO: add stop to tree iterator...
+ [self.writeTree forEach:^(FPath *path, id value) {
+ if (!stop) {
+ block(path, value, &stop);
+ }
+ }];
+}
+
+/**
+* Applies this FCompoundWrite to a node. The node is returned with all writes from this FCompoundWrite applied to the node.
+* @param node The node to apply this FCompoundWrite to
+* @return The node with all writes applied
+*/
+- (id<FNode>) applyToNode:(id<FNode>)node {
+ return [self applySubtreeWrite:self.writeTree atPath:[FPath empty] toNode:node];
+}
+
+/**
+* Return true if this CompoundWrite is empty and therefore does not modify any nodes.
+* @return Whether this CompoundWrite is empty
+*/
+- (BOOL) isEmpty {
+ return self.writeTree.isEmpty;
+}
+
+- (id<FNode>) rootWrite {
+ return self.writeTree.value;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FCompoundWrite class]]) {
+ return NO;
+ }
+ FCompoundWrite *other = (FCompoundWrite *)object;
+ return [[self valForExport:YES] isEqualToDictionary:[other valForExport:YES]];
+}
+
+- (NSUInteger)hash {
+ return [[self valForExport:YES] hash];
+}
+
+- (NSDictionary *)valForExport:(BOOL)exportFormat {
+ NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
+ [self.writeTree forEach:^(FPath *path, id<FNode> value) {
+ dictionary[path.wireFormat] = [value valForExport:exportFormat];
+ }];
+ return dictionary;
+}
+
+- (NSString *)description {
+ return [[self valForExport:YES] description];
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FEmptyNode.h b/Firebase/Database/Snapshot/FEmptyNode.h
new file mode 100644
index 0000000..ab404c2
--- /dev/null
+++ b/Firebase/Database/Snapshot/FEmptyNode.h
@@ -0,0 +1,24 @@
+/*
+ * 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 FEmptyNode : NSObject
+
++ (id<FNode>) emptyNode;
+
+@end
diff --git a/Firebase/Database/Snapshot/FEmptyNode.m b/Firebase/Database/Snapshot/FEmptyNode.m
new file mode 100644
index 0000000..dd2d9ea
--- /dev/null
+++ b/Firebase/Database/Snapshot/FEmptyNode.m
@@ -0,0 +1,29 @@
+/*
+ * 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 "FEmptyNode.h"
+#import "FChildrenNode.h"
+
+@implementation FEmptyNode
+
++ (id<FNode>) emptyNode {
+ static FChildrenNode* empty = nil;
+ if (empty == nil) {
+ empty = [[FChildrenNode alloc] init];
+ }
+ return empty;
+}
+@end
diff --git a/Firebase/Database/Snapshot/FIndexedNode.h b/Firebase/Database/Snapshot/FIndexedNode.h
new file mode 100644
index 0000000..fd2db37
--- /dev/null
+++ b/Firebase/Database/Snapshot/FIndexedNode.h
@@ -0,0 +1,49 @@
+/*
+ * 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 "FIndex.h"
+#import "FNamedNode.h"
+
+/**
+ * Represents a node together with an index. The index and node are updated in unison. In the case where the index
+ * does not affect the ordering (i.e. the ordering is identical to the key ordering) this class uses a fallback index
+ * to save memory. Everything operating on the index must special case the fallback index.
+ */
+@interface FIndexedNode : NSObject
+
+@property (nonatomic, strong, readonly) id<FNode> node;
+
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node;
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node index:(id<FIndex>)index;
+
+- (BOOL)hasIndex:(id<FIndex>)index;
+- (FIndexedNode *)updateChild:(NSString *)key withNewChild:(id<FNode>)newChildNode;
+- (FIndexedNode *)updatePriority:(id<FNode>)priority;
+
+- (FNamedNode *)firstChild;
+- (FNamedNode *)lastChild;
+
+- (NSString *)predecessorForChildKey:(NSString *)childKey childNode:(id<FNode>)childNode index:(id<FIndex>)index;
+
+- (void)enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *key, id<FNode> node, BOOL *stop))block;
+
+- (NSEnumerator *)childEnumerator;
+
+@end
diff --git a/Firebase/Database/Snapshot/FIndexedNode.m b/Firebase/Database/Snapshot/FIndexedNode.m
new file mode 100644
index 0000000..e874dcf
--- /dev/null
+++ b/Firebase/Database/Snapshot/FIndexedNode.m
@@ -0,0 +1,202 @@
+/*
+ * 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 "FIndexedNode.h"
+
+#import "FImmutableSortedSet.h"
+#import "FIndex.h"
+#import "FPriorityIndex.h"
+#import "FKeyIndex.h"
+#import "FChildrenNode.h"
+
+static FImmutableSortedSet *FALLBACK_INDEX;
+
+@interface FIndexedNode ()
+
+@property (nonatomic, strong) id<FNode> node;
+/**
+ * The indexed set is initialized lazily to prevent creation when it is not needed
+ */
+@property (nonatomic, strong) FImmutableSortedSet *indexed;
+@property (nonatomic, strong) id<FIndex> index;
+
+@end
+
+@implementation FIndexedNode
+
++ (FImmutableSortedSet *)fallbackIndex {
+ static FImmutableSortedSet *fallbackIndex;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ fallbackIndex = [[FImmutableSortedSet alloc] init];
+ });
+ return fallbackIndex;
+}
+
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node
+{
+ return [[FIndexedNode alloc] initWithNode:node index:[FPriorityIndex priorityIndex]];
+}
+
++ (FIndexedNode *)indexedNodeWithNode:(id<FNode>)node index:(id<FIndex>)index
+{
+ return [[FIndexedNode alloc] initWithNode:node index:index];
+}
+
+- (id)initWithNode:(id<FNode>)node index:(id<FIndex>)index
+{
+ // Initialize indexed lazily
+ return [self initWithNode:node index:index indexed:nil];
+}
+
+- (id)initWithNode:(id<FNode>)node index:(id<FIndex>)index indexed:(FImmutableSortedSet *)indexed
+{
+ self = [super init];
+ if (self != nil) {
+ self->_node = node;
+ self->_index = index;
+ self->_indexed = indexed;
+ }
+ return self;
+}
+
+- (void)ensureIndexed
+{
+ if (!self.indexed) {
+ if ([self.index isEqual:[FKeyIndex keyIndex]]) {
+ self.indexed = [FIndexedNode fallbackIndex];
+ } else {
+ __block BOOL sawChild;
+ [self.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ sawChild = sawChild || [self.index isDefinedOn:node];
+ *stop = sawChild;
+ }];
+ if (sawChild) {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ [self.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
+ FNamedNode *namedNode = [[FNamedNode alloc] initWithName:key andNode:node];
+ dict[namedNode] = [NSNull null];
+ }];
+ // Make sure to assign index here, because the comparator will be retained and using self will cause a
+ // cycle
+ id<FIndex> index = self.index;
+ self.indexed = [FImmutableSortedSet setWithKeysFromDictionary:dict
+ comparator:^NSComparisonResult(FNamedNode *namedNode1, FNamedNode *namedNode2) {
+ return [index compareNamedNode:namedNode1 toNamedNode:namedNode2];
+ }];
+ } else {
+ self.indexed = [FIndexedNode fallbackIndex];
+ }
+ }
+ }
+}
+
+- (BOOL)hasIndex:(id<FIndex>)index
+{
+ return [self.index isEqual:index];
+}
+
+- (FIndexedNode *)updateChild:(NSString *)key withNewChild:(id<FNode>)newChildNode
+{
+ id<FNode> newNode = [self.node updateImmediateChild:key withNewChild:newChildNode];
+ if (self.indexed == [FIndexedNode fallbackIndex] && ![self.index isDefinedOn:newChildNode]) {
+ // doesn't affect the index, no need to create an index
+ return [[FIndexedNode alloc] initWithNode:newNode index:self.index indexed:[FIndexedNode fallbackIndex]];
+ } else if (!self.indexed || self.indexed == [FIndexedNode fallbackIndex]) {
+ // No need to index yet, index lazily
+ return [[FIndexedNode alloc] initWithNode:newNode index:self.index];
+ } else {
+ id<FNode> oldChild = [self.node getImmediateChild:key];
+ FImmutableSortedSet *newIndexed = [self.indexed removeObject:[FNamedNode nodeWithName:key node:oldChild]];
+ if (![newChildNode isEmpty]) {
+ newIndexed = [newIndexed addObject:[FNamedNode nodeWithName:key node:newChildNode]];
+ }
+ return [[FIndexedNode alloc] initWithNode:newNode index:self.index indexed:newIndexed];
+ }
+}
+
+- (FIndexedNode *)updatePriority:(id<FNode>)priority
+{
+ return [[FIndexedNode alloc] initWithNode:[self.node updatePriority:priority]
+ index:self.index
+ indexed:self.indexed];
+}
+
+- (FNamedNode *)firstChild
+{
+ if (![self.node isKindOfClass:[FChildrenNode class]]) {
+ return nil;
+ } else {
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [((FChildrenNode *)self.node) firstChild];
+ } else {
+ return self.indexed.firstObject;
+ }
+ }
+}
+
+- (FNamedNode *)lastChild
+{
+ if (![self.node isKindOfClass:[FChildrenNode class]]) {
+ return nil;
+ } else {
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [((FChildrenNode *)self.node) lastChild];
+ } else {
+ return self.indexed.lastObject;
+ }
+ }
+}
+
+- (NSString *)predecessorForChildKey:(NSString *)childKey childNode:(id<FNode>)childNode index:(id<FIndex>)index
+{
+ if (![self.index isEqual:index]) {
+ [NSException raise:NSInvalidArgumentException format:@"Index not available in IndexedNode!"];
+ }
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [self.node predecessorChildKey:childKey];
+ } else {
+ FNamedNode *node = [self.indexed predecessorEntry:[FNamedNode nodeWithName:childKey node:childNode]];
+ return node.name;
+ }
+}
+
+- (void)enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ [self.node enumerateChildrenReverse:reverse usingBlock:block];
+ } else {
+ [self.indexed enumerateObjectsReverse:reverse usingBlock:^(FNamedNode *namedNode, BOOL *stop) {
+ block(namedNode.name, namedNode.node, stop);
+ }];
+ }
+}
+
+- (NSEnumerator *)childEnumerator
+{
+ [self ensureIndexed];
+ if (self.indexed == [FIndexedNode fallbackIndex]) {
+ return [self.node childEnumerator];
+ } else {
+ return [self.indexed objectEnumerator];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Snapshot/FLeafNode.h b/Firebase/Database/Snapshot/FLeafNode.h
new file mode 100644
index 0000000..15e0132
--- /dev/null
+++ b/Firebase/Database/Snapshot/FLeafNode.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 "FNode.h"
+
+
+@interface FLeafNode : NSObject <FNode>
+
+- (id)initWithValue:(id)aValue;
+- (id)initWithValue:(id)aValue withPriority:(id<FNode>)aPriority;
+
+@property (nonatomic, strong) id value;
+
+@end
diff --git a/Firebase/Database/Snapshot/FLeafNode.m b/Firebase/Database/Snapshot/FLeafNode.m
new file mode 100644
index 0000000..a26e057
--- /dev/null
+++ b/Firebase/Database/Snapshot/FLeafNode.m
@@ -0,0 +1,250 @@
+/*
+ * 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 "FLeafNode.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FConstants.h"
+#import "FImmutableSortedDictionary.h"
+#import "FUtilities.h"
+#import "FStringUtilities.h"
+#import "FSnapshotUtilities.h"
+
+@interface FLeafNode ()
+@property (nonatomic, strong) id<FNode> priorityNode;
+@property (nonatomic, strong) NSString *lazyHash;
+
+@end
+
+@implementation FLeafNode
+
+@synthesize value;
+@synthesize priorityNode;
+
+- (id)initWithValue:(id)aValue {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.priorityNode = [FEmptyNode emptyNode];
+ }
+ return self;
+}
+
+- (id)initWithValue:(id)aValue withPriority:(id<FNode>)aPriority {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ [FSnapshotUtilities validatePriorityNode:aPriority];
+ self.priorityNode = aPriority;
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark FNode methods
+
+- (BOOL) isLeafNode {
+ return YES;
+}
+
+- (id<FNode>) getPriority {
+ return self.priorityNode;
+}
+
+- (id<FNode>) updatePriority:(id<FNode>)aPriority {
+ return [[FLeafNode alloc] initWithValue:self.value withPriority:aPriority];
+}
+
+- (id<FNode>) getImmediateChild:(NSString *) childName {
+ if ([childName isEqualToString:@".priority"]) {
+ return self.priorityNode;
+ } else {
+ return [FEmptyNode emptyNode];
+ }
+}
+
+- (id<FNode>) getChild:(FPath *)path {
+ if (path.getFront == nil) {
+ return self;
+ } else if ([[path getFront] isEqualToString:@".priority"]) {
+ return [self getPriority];
+ } else {
+ return [FEmptyNode emptyNode];
+ }
+}
+
+- (BOOL)hasChild:(NSString *)childName {
+ return [childName isEqualToString:@".priority"] && ![self getPriority].isEmpty;
+}
+
+
+- (NSString *)predecessorChildKey:(NSString *)childKey
+{
+ return nil;
+}
+
+- (id<FNode>) updateImmediateChild:(NSString *)childName withNewChild:(id<FNode>)newChildNode {
+ if ([childName isEqualToString:@".priority"]) {
+ return [self updatePriority:newChildNode];
+ } else if (newChildNode.isEmpty) {
+ return self;
+ } else {
+ FChildrenNode* childrenNode = [[FChildrenNode alloc] init];
+ childrenNode = [childrenNode updateImmediateChild:childName withNewChild:newChildNode];
+ childrenNode = [childrenNode updatePriority:self.priorityNode];
+ return childrenNode;
+ }
+}
+
+- (id<FNode>) updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode {
+ NSString* front = [path getFront];
+ if(front == nil) {
+ return newChildNode;
+ } else if (newChildNode.isEmpty && ![front isEqualToString:@".priority"]) {
+ return self;
+ } else {
+ NSAssert(![front isEqualToString:@".priority"] || path.length == 1, @".priority must be the last token in a path.");
+ return [self updateImmediateChild:front withNewChild:
+ [[FEmptyNode emptyNode] updateChild:[path popFront] withNewChild:newChildNode]];
+ }
+}
+
+- (id) val {
+ return [self valForExport:NO];
+}
+
+- (id) valForExport:(BOOL)exp {
+ if(exp && !self.getPriority.isEmpty) {
+ return @{kPayloadValue : self.value,
+ kPayloadPriority : [[self getPriority] val]};
+ }
+ else {
+ return self.value;
+ }
+}
+
+- (BOOL)isEqual:(id <FNode>)other {
+ if(other == self) {
+ return YES;
+ } else if (other.isLeafNode) {
+ FLeafNode *otherLeaf = other;
+ if ([FUtilities getJavascriptType:self.value] != [FUtilities getJavascriptType:otherLeaf.value]) {
+ return NO;
+ }
+ return [otherLeaf.value isEqual:self.value] && [otherLeaf.priorityNode isEqual:self.priorityNode];
+ } else {
+ return NO;
+ }
+}
+
+- (NSUInteger)hash {
+ return [self.value hash] * 17 + self.priorityNode.hash;
+}
+
+- (id <FNode>)withIndex:(id <FIndex>)index {
+ return self;
+}
+
+- (BOOL)isIndexed:(id <FIndex>)index {
+ return YES;
+}
+
+- (BOOL) isEmpty {
+ return NO;
+}
+
+- (int) numChildren {
+ return 0;
+}
+
+- (void) enumerateChildrenUsingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ // Nothing to iterate over
+}
+
+- (void) enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *, id<FNode>, BOOL *))block
+{
+ // Nothing to iterate over
+}
+
+- (NSEnumerator *)childEnumerator
+{
+ // Nothing to iterate over
+ return [@[] objectEnumerator];
+}
+
+- (NSString *) dataHash {
+ if (self.lazyHash == nil) {
+ NSMutableString *toHash = [[NSMutableString alloc] init];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:self toString:toHash hashVersion:FDataHashVersionV1];
+
+ self.lazyHash = [FStringUtilities base64EncodedSha1:toHash];
+ }
+ return self.lazyHash;
+}
+
+- (NSComparisonResult)compare:(id <FNode>)other {
+ if (other == [FEmptyNode emptyNode]) {
+ return NSOrderedDescending;
+ } else if ([other isKindOfClass:[FChildrenNode class]]) {
+ return NSOrderedAscending;
+ } else {
+ NSAssert(other.isLeafNode, @"Compared against unknown type of node.");
+ return [self compareToLeafNode:(FLeafNode*)other];
+ }
+}
+
++ (NSArray*) valueTypeOrder {
+ static NSArray* valueOrder = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ valueOrder = @[kJavaScriptObject, kJavaScriptBoolean, kJavaScriptNumber, kJavaScriptString];
+ });
+ return valueOrder;
+}
+
+- (NSComparisonResult) compareToLeafNode:(FLeafNode*)other {
+ NSString* thisLeafType = [FUtilities getJavascriptType:self.value];
+ NSString* otherLeafType = [FUtilities getJavascriptType:other.value];
+ NSUInteger thisIndex = [[FLeafNode valueTypeOrder] indexOfObject:thisLeafType];
+ NSUInteger otherIndex = [[FLeafNode valueTypeOrder] indexOfObject:otherLeafType];
+ assert(thisIndex >= 0 && otherIndex >= 0);
+ if (otherIndex == thisIndex) {
+ // Same type. Compare values.
+ if (thisLeafType == kJavaScriptObject) {
+ // Deferred value nodes are all equal, but we should also never get to this point...
+ return NSOrderedSame;
+ } else if (thisLeafType == kJavaScriptString) {
+ return [self.value compare:other.value options:NSLiteralSearch];
+ } else {
+ return [self.value compare:other.value];
+ }
+ } else {
+ return thisIndex > otherIndex ? NSOrderedDescending : NSOrderedAscending;
+ }
+}
+
+- (NSString *) description {
+ return [[self valForExport:YES] description];
+}
+
+- (void) forEachChildDo:(fbt_bool_nsstring_node)action {
+ // There are no children, so there is nothing to do.
+ return;
+}
+
+
+@end
diff --git a/Firebase/Database/Snapshot/FNode.h b/Firebase/Database/Snapshot/FNode.h
new file mode 100644
index 0000000..1316756
--- /dev/null
+++ b/Firebase/Database/Snapshot/FNode.h
@@ -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 <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FTypedefs_Private.h"
+
+@protocol FIndex;
+
+@protocol FNode <NSObject>
+
+- (BOOL) isLeafNode;
+- (id<FNode>) getPriority;
+- (id<FNode>) updatePriority:(id<FNode>)priority;
+- (id<FNode>) getImmediateChild:(NSString *)childKey;
+- (id<FNode>) getChild:(FPath *)path;
+- (NSString *) predecessorChildKey:(NSString *)childKey;
+- (id<FNode>) updateImmediateChild:(NSString *)childKey withNewChild:(id<FNode>)newChildNode;
+- (id<FNode>) updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode;
+- (BOOL) hasChild:(NSString*)childKey;
+- (BOOL) isEmpty;
+- (int) numChildren;
+- (id) val;
+- (id) valForExport:(BOOL)exp;
+- (NSString *) dataHash;
+- (NSComparisonResult) compare:(id<FNode>)other;
+- (BOOL) isEqual:(id<FNode>)other;
+- (void)enumerateChildrenUsingBlock:(void (^)(NSString *key, id<FNode> node, BOOL *stop))block;
+- (void)enumerateChildrenReverse:(BOOL)reverse usingBlock:(void (^)(NSString *key, id<FNode> node, BOOL *stop))block;
+
+- (NSEnumerator *)childEnumerator;
+
+@end
diff --git a/Firebase/Database/Snapshot/FSnapshotUtilities.h b/Firebase/Database/Snapshot/FSnapshotUtilities.h
new file mode 100644
index 0000000..2a28788
--- /dev/null
+++ b/Firebase/Database/Snapshot/FSnapshotUtilities.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>
+#import "FNode.h"
+
+@class FImmutableSortedDictionary;
+@class FCompoundWrite;
+@class FLeafNode;
+@protocol FNode;
+
+typedef NS_ENUM(NSInteger, FDataHashVersion) {
+ FDataHashVersionV1,
+ FDataHashVersionV2,
+};
+
+@interface FSnapshotUtilities : NSObject
+
++ (id<FNode>) nodeFrom:(id)val;
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority;
++ (id<FNode>) nodeFrom:(id)val withValidationFrom:(NSString *)fn;
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority withValidationFrom:(NSString *)fn;
++ (FCompoundWrite *) compoundWriteFromDictionary:(NSDictionary *)values withValidationFrom:(NSString *)fn;
++ (void) validatePriorityNode:(id<FNode>)priorityNode;
++ (void)appendHashRepresentationForLeafNode:(FLeafNode *)val
+ toString:(NSMutableString *)string
+ hashVersion:(FDataHashVersion)hashVersion;
++ (void)appendHashV2RepresentationForString:(NSString *)string toString:(NSMutableString *)mutableString;
+
++ (NSUInteger)estimateSerializedNodeSize:(id<FNode>)node;
+
+@end
diff --git a/Firebase/Database/Snapshot/FSnapshotUtilities.m b/Firebase/Database/Snapshot/FSnapshotUtilities.m
new file mode 100644
index 0000000..1b83430
--- /dev/null
+++ b/Firebase/Database/Snapshot/FSnapshotUtilities.m
@@ -0,0 +1,301 @@
+/*
+ * 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 "FSnapshotUtilities.h"
+#import "FEmptyNode.h"
+#import "FLeafNode.h"
+#import "FConstants.h"
+#import "FUtilities.h"
+#import "FChildrenNode.h"
+#import "FLLRBValueNode.h"
+#import "FValidation.h"
+#import "FMaxNode.h"
+#import "FNamedNode.h"
+#import "FCompoundWrite.h"
+
+@implementation FSnapshotUtilities
+
++ (id<FNode>) nodeFrom:(id)val {
+ return [FSnapshotUtilities nodeFrom:val priority:nil];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority {
+ return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:@"nodeFrom:priority:"];
+}
+
++ (id<FNode>) nodeFrom:(id)val withValidationFrom:(NSString *)fn {
+ return [FSnapshotUtilities nodeFrom:val priority:nil withValidationFrom:fn];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)priority withValidationFrom:(NSString *)fn {
+ return [FSnapshotUtilities nodeFrom:val priority:priority withValidationFrom:fn atDepth:0 path:[[NSMutableArray alloc] init]];
+}
+
++ (id<FNode>) nodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
+ @autoreleasepool {
+ return [FSnapshotUtilities internalNodeFrom:val priority:aPriority withValidationFrom:fn atDepth:depth path:path];
+ }
+}
+
++ (id<FNode>) internalNodeFrom:(id)val priority:(id)aPriority withValidationFrom:(NSString *)fn atDepth:(int)depth path:(NSMutableArray *)path {
+
+
+ if (depth > kFirebaseMaxObjectDepth) {
+ NSRange range;
+ range.location = 0;
+ range.length = 100;
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Max object depth exceeded: %@...", fn, pathString] userInfo:nil];
+ }
+
+ if (val == nil || val == [NSNull null]) {
+ // Null is a valid type to store
+ return [FEmptyNode emptyNode];
+ }
+
+ [FValidation validateFrom:fn isValidPriorityValue:aPriority withPath:path];
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:aPriority];
+
+ id value = val;
+ BOOL isLeafNode = NO;
+
+ if([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dict = val;
+ if(dict[kPayloadPriority] != nil) {
+ id rawPriority = [dict objectForKey:kPayloadPriority];
+ [FValidation validateFrom:fn isValidPriorityValue:rawPriority withPath:path];
+ priority = [FSnapshotUtilities nodeFrom:rawPriority];
+ }
+
+ if(dict[kPayloadValue] != nil) {
+ value = [dict objectForKey:kPayloadValue];
+ if ([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
+ isLeafNode = YES;
+ } else {
+ @throw [[NSException alloc]
+ initWithName:@"InvalidLeafValueType"
+ reason:[NSString stringWithFormat:@"(%@) Invalid data type used with .value. Can only use "
+ "NSString and NSNumber or be null. Found %@ instead.",
+ fn, [[value class] description]] userInfo:nil];
+ }
+ }
+ }
+
+ if([FValidation validateFrom:fn isValidLeafValue:value withPath:path]) {
+ isLeafNode = YES;
+ }
+
+ if (isLeafNode) {
+ return [[FLeafNode alloc] initWithValue:value withPriority:priority];
+ }
+
+ // Unlike with JS, we have to handle the dictionary and array cases separately.
+ if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dval = (NSDictionary *)value;
+ NSMutableDictionary *children = [NSMutableDictionary dictionaryWithCapacity:dval.count];
+
+ // Avoid creating a million newPaths by appending to old one
+ for (id keyId in dval) {
+ [FValidation validateFrom:fn validDictionaryKey:keyId withPath:path];
+ NSString* key = (NSString*)keyId;
+
+ if (![key hasPrefix:kPayloadMetadataPrefix]) {
+ [path addObject:key];
+ id<FNode> childNode = [FSnapshotUtilities nodeFrom:dval[key] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
+ [path removeLastObject];
+
+ if (![childNode isEmpty]) {
+ children[key] = childNode;
+ }
+ }
+ }
+
+ if ([children count] == 0) {
+ return [FEmptyNode emptyNode];
+ } else {
+ FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
+ withComparator:[FUtilities keyComparator]];
+ return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
+ }
+ } else if([value isKindOfClass:[NSArray class]]) {
+ NSArray* aval = (NSArray *)value;
+ NSMutableDictionary* children = [NSMutableDictionary dictionaryWithCapacity:aval.count];
+
+ for(int i = 0; i < [aval count]; i++) {
+ NSString* key = [NSString stringWithFormat:@"%i", i];
+ [path addObject:key];
+ id<FNode> childNode = [FSnapshotUtilities nodeFrom:[aval objectAtIndex:i] priority:nil withValidationFrom:fn atDepth:depth + 1 path:path];
+ [path removeLastObject];
+
+ if (![childNode isEmpty]) {
+ children[key] = childNode;
+ }
+ }
+
+ if ([children count] == 0) {
+ return [FEmptyNode emptyNode];
+ } else {
+ FImmutableSortedDictionary *childrenDict = [FImmutableSortedDictionary fromDictionary:children
+ withComparator:[FUtilities keyComparator]];
+ return [[FChildrenNode alloc] initWithPriority:priority children:childrenDict];
+ }
+ } else {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData"
+ reason:[NSString stringWithFormat:@"(%@) Cannot store object of type %@ at %@. "
+ "Can only store objects of type NSNumber, NSString, NSDictionary, and NSArray.",
+ fn, [[value class] description], pathString] userInfo:nil];
+ }
+}
+
++ (FCompoundWrite *) compoundWriteFromDictionary:(NSDictionary *)values withValidationFrom:(NSString *)fn {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+
+ NSMutableArray *updatePaths = [NSMutableArray arrayWithCapacity:values.count];
+ for (NSString *keyId in values) {
+ id value = values[keyId];
+ [FValidation validateFrom:fn validUpdateDictionaryKey:keyId withValue:value];
+
+ FPath* path = [FPath pathWithString:keyId];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:value withValidationFrom:fn];
+
+ [updatePaths addObject:path];
+ compoundWrite = [compoundWrite addWrite:node atPath:path];
+ }
+
+ // Check that the update paths are not descendants of each other.
+ [updatePaths sortUsingComparator:^NSComparisonResult(FPath* left, FPath* right) {
+ return [left compare:right];
+ }];
+ FPath *prevPath = nil;
+ for (FPath *path in updatePaths) {
+ if (prevPath != nil && [prevPath contains:path]) {
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid path in object. Path (%@) is an ancestor of (%@).", fn, prevPath, path] userInfo:nil];
+ }
+ prevPath = path;
+ }
+
+ return compoundWrite;
+}
+
++ (void)validatePriorityNode:(id <FNode>)priorityNode {
+ assert(priorityNode != nil);
+ if (priorityNode.isLeafNode) {
+ id val = priorityNode.val;
+ if ([val isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* valDict __unused = (NSDictionary*)val;
+ NSAssert(valDict[kServerValueSubKey] != nil, @"Priority can't be object unless it's a deferred value");
+ } else {
+ NSString *jsType __unused = [FUtilities getJavascriptType:val];
+ NSAssert(jsType == kJavaScriptString || jsType == kJavaScriptNumber, @"Priority of unexpected type.");
+ }
+ } else {
+ NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.isEmpty, @"Priority of unexpected type.");
+ }
+ // Don't call getPriority() on MAX_NODE to avoid hitting assertion.
+ NSAssert(priorityNode == [FMaxNode maxNode] || priorityNode.getPriority.isEmpty,
+ @"Priority nodes can't have a priority of their own.");
+}
+
++ (void)appendHashRepresentationForLeafNode:(FLeafNode *)leafNode
+ toString:(NSMutableString *)string
+ hashVersion:(FDataHashVersion)hashVersion {
+ NSAssert(hashVersion == FDataHashVersionV1 || hashVersion == FDataHashVersionV2,
+ @"Unknown hash version: %lu", (unsigned long)hashVersion);
+ if (!leafNode.getPriority.isEmpty) {
+ [string appendString:@"priority:"];
+ [FSnapshotUtilities appendHashRepresentationForLeafNode:leafNode.getPriority toString:string hashVersion:hashVersion];
+ [string appendString:@":"];
+ }
+
+ NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
+ [string appendString:jsType];
+ [string appendString:@":"];
+
+
+ if (jsType == kJavaScriptBoolean) {
+ NSString *boolString = [leafNode.val boolValue] ? kJavaScriptTrue : kJavaScriptFalse;
+ [string appendString:boolString];
+ } else if (jsType == kJavaScriptNumber) {
+ NSString *numberString = [FUtilities ieee754StringForNumber:leafNode.val];
+ [string appendString:numberString];
+ } else if (jsType == kJavaScriptString) {
+ if (hashVersion == FDataHashVersionV1) {
+ [string appendString:leafNode.val];
+ } else {
+ NSAssert(hashVersion == FDataHashVersionV2, @"Invalid hash version found");
+ [FSnapshotUtilities appendHashV2RepresentationForString:leafNode.val toString:string];
+ }
+ } else {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown value for hashing: %@", leafNode];
+ }
+}
+
++ (void)appendHashV2RepresentationForString:(NSString *)string
+ toString:(NSMutableString *)mutableString {
+ string = [string stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
+ string = [string stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
+ [mutableString appendString:@"\""];
+ [mutableString appendString:string];
+ [mutableString appendString:@"\""];
+}
+
++ (NSUInteger)estimateLeafNodeSize:(FLeafNode *)leafNode {
+ NSString *jsType = [FUtilities getJavascriptType:leafNode.val];
+ // These values are somewhat arbitrary, but we don't need an exact value so prefer performance over exact value
+ NSUInteger valueSize;
+ if (jsType == kJavaScriptNumber) {
+ valueSize = 8; // estimate each float with 8 bytes
+ } else if (jsType == kJavaScriptBoolean) {
+ valueSize = 4; // true or false need roughly 4 bytes
+ } else if (jsType == kJavaScriptString) {
+ valueSize = 2 + [leafNode.val length]; // add 2 for quotes
+ } else {
+ [NSException raise:NSInvalidArgumentException format:@"Unknown leaf type: %@", leafNode];
+ return 0;
+ }
+
+ if (leafNode.getPriority.isEmpty) {
+ return valueSize;
+ } else {
+ // Account for extra overhead due to the extra JSON object and the ".value" and ".priority" keys, colons, comma
+ NSUInteger leafPriorityOverhead = 2 + 8 + 11 + 2 + 1;
+ return leafPriorityOverhead + valueSize + [FSnapshotUtilities estimateLeafNodeSize:leafNode.getPriority];
+ }
+}
+
++ (NSUInteger)estimateSerializedNodeSize:(id<FNode>)node {
+ if ([node isEmpty]) {
+ return 4; // null keyword
+ } else if ([node isLeafNode]) {
+ return [FSnapshotUtilities estimateLeafNodeSize:node];
+ } else {
+ NSAssert([node isKindOfClass:[FChildrenNode class]], @"Unexpected node type: %@", [node class]);
+ __block NSUInteger sum = 1; // opening brackets
+ [((FChildrenNode *)node) enumerateChildrenAndPriorityUsingBlock:^(NSString *key, id<FNode>child, BOOL *stop) {
+ sum += key.length;
+ sum += 4; // quotes around key and colon and (comma or closing bracket)
+ sum += [FSnapshotUtilities estimateSerializedNodeSize:child];
+ }];
+ return sum;
+ }
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FAtomicNumber.h b/Firebase/Database/Utilities/FAtomicNumber.h
new file mode 100644
index 0000000..589dc25
--- /dev/null
+++ b/Firebase/Database/Utilities/FAtomicNumber.h
@@ -0,0 +1,23 @@
+/*
+ * 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 FAtomicNumber : NSObject
+
+- (NSNumber *) getAndIncrement;
+
+@end
diff --git a/Firebase/Database/Utilities/FAtomicNumber.m b/Firebase/Database/Utilities/FAtomicNumber.m
new file mode 100644
index 0000000..be0e537
--- /dev/null
+++ b/Firebase/Database/Utilities/FAtomicNumber.m
@@ -0,0 +1,54 @@
+/*
+ * 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 "FAtomicNumber.h"
+
+@interface FAtomicNumber() {
+ unsigned long number;
+}
+
+@property (nonatomic, strong) NSLock* lock;
+
+@end
+
+@implementation FAtomicNumber
+
+@synthesize lock;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ number = 1;
+ self.lock = [[NSLock alloc] init];
+ }
+ return self;
+}
+
+- (NSNumber *) getAndIncrement {
+ NSNumber* result;
+
+ // See: http://developer.apple.com/library/ios/#DOCUMENTATION/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-SW14 to improve, etc.
+
+ [self.lock lock];
+ result = [NSNumber numberWithUnsignedLong:number];
+ number = number + 1;
+ [self.lock unlock];
+
+ return result;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FEventEmitter.h b/Firebase/Database/Utilities/FEventEmitter.h
new file mode 100644
index 0000000..069e10f
--- /dev/null
+++ b/Firebase/Database/Utilities/FEventEmitter.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 "FIRDatabaseQuery.h"
+#import "FIRDatabaseConfig.h"
+#import "FTypedefs_Private.h"
+
+@interface FEventEmitter : NSObject
+
+- (id) initWithAllowedEvents:(NSArray *)theAllowedEvents queue:(dispatch_queue_t)queue;
+
+- (id) getInitialEventForType:(NSString *)eventType;
+- (void) triggerEventType:(NSString *)eventType data:(id)data;
+
+- (FIRDatabaseHandle)observeEventType:(NSString *)eventType withBlock:(fbt_void_id)block;
+- (void) removeObserverForEventType:(NSString *)eventType withHandle:(FIRDatabaseHandle)handle;
+
+- (void) validateEventType:(NSString *)eventType;
+
+@end
diff --git a/Firebase/Database/Utilities/FEventEmitter.m b/Firebase/Database/Utilities/FEventEmitter.m
new file mode 100644
index 0000000..f7c569b
--- /dev/null
+++ b/Firebase/Database/Utilities/FEventEmitter.m
@@ -0,0 +1,145 @@
+/*
+ * 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 "FEventEmitter.h"
+#import "FUtilities.h"
+#import "FRepoManager.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@interface FEventListener : NSObject
+
+@property (nonatomic, copy) fbt_void_id userCallback;
+@property (nonatomic) FIRDatabaseHandle handle;
+
+@end
+
+@implementation FEventListener
+
+@synthesize userCallback;
+@synthesize handle;
+
+@end
+
+
+@interface FEventEmitter ()
+
+@property (nonatomic, strong) NSArray *allowedEvents;
+@property (nonatomic, strong) NSMutableDictionary *listeners;
+@property (nonatomic, strong) dispatch_queue_t queue;
+
+@end
+
+
+@implementation FEventEmitter
+
+@synthesize allowedEvents;
+@synthesize listeners;
+
+- (id) initWithAllowedEvents:(NSArray *)theAllowedEvents queue:(dispatch_queue_t)queue {
+ if (theAllowedEvents == nil || [theAllowedEvents count] == 0) {
+ @throw [NSException exceptionWithName:@"AllowedEventsValidation" reason:@"FEventEmitters must be initialized with at least one valid event." userInfo:nil];
+ }
+
+ self = [super init];
+
+ if (self) {
+ self.allowedEvents = [theAllowedEvents copy];
+ self.listeners = [[NSMutableDictionary alloc] init];
+ self.queue = queue;
+ }
+
+ return self;
+}
+
+- (id) getInitialEventForType:(NSString *)eventType {
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"You must override getInitialEvent: when subclassing FEventEmitter" userInfo:nil];
+}
+
+- (void) triggerEventType:(NSString *)eventType data:(id)data {
+ [self validateEventType:eventType];
+ NSMutableDictionary *eventTypeListeners = [self.listeners objectForKey:eventType];
+ for (FEventListener *listener in eventTypeListeners) {
+ [self triggerListener:listener withData:data];
+ }
+}
+
+- (void) triggerListener:(FEventListener *)listener withData:(id)data {
+ // TODO, should probably get this from FRepo or something although it ends up being the same. (Except maybe for testing)
+ if (listener.userCallback) {
+ dispatch_async(self.queue, ^{
+ listener.userCallback(data);
+ });
+ }
+}
+
+- (FIRDatabaseHandle)observeEventType:(NSString *)eventType withBlock:(fbt_void_id)block {
+ [self validateEventType:eventType];
+
+ // Create listener
+ FEventListener *listener = [[FEventListener alloc] init];
+ listener.handle = [[FUtilities LUIDGenerator] integerValue];
+ listener.userCallback = block; // copies block automatically
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self addEventListener:listener forEventType:eventType];
+ });
+
+ return listener.handle;
+}
+
+- (void) addEventListener:(FEventListener *)listener forEventType:(NSString *)eventType {
+ // Get or initializer listeners map [FIRDatabaseHandle -> callback block] for eventType
+ NSMutableArray *eventTypeListeners = [self.listeners objectForKey:eventType];
+ if (eventTypeListeners == nil) {
+ eventTypeListeners = [[NSMutableArray alloc] init];
+ [self.listeners setObject:eventTypeListeners forKey:eventType];
+ }
+
+ // Add listener and fire the current event for this listener
+ [eventTypeListeners addObject:listener];
+ id initialData = [self getInitialEventForType:eventType];
+ [self triggerListener:listener withData:initialData];
+}
+
+- (void) removeObserverForEventType:(NSString *)eventType withHandle:(FIRDatabaseHandle)handle {
+ [self validateEventType:eventType];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self removeEventListenerWithHandle:handle forEventType:eventType];
+ });
+}
+
+- (void)removeEventListenerWithHandle:(FIRDatabaseHandle)handle forEventType:(NSString *)eventType {
+ NSMutableArray *eventTypeListeners = [self.listeners objectForKey:eventType];
+ for (FEventListener *listener in [eventTypeListeners copy]) {
+ if (handle == NSNotFound || handle == listener.handle) {
+ [eventTypeListeners removeObject:listener];
+ }
+ }
+}
+
+
+- (void) validateEventType:(NSString *)eventType {
+ if ([self.allowedEvents indexOfObject:eventType] == NSNotFound) {
+ @throw [NSException exceptionWithName:@"InvalidEventType"
+ reason:[NSString stringWithFormat:@"%@ is not a valid event type. %@ is the list of valid events.",
+ eventType, self.allowedEvents]
+ userInfo:nil];
+ }
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FNextPushId.h b/Firebase/Database/Utilities/FNextPushId.h
new file mode 100644
index 0000000..2da54f0
--- /dev/null
+++ b/Firebase/Database/Utilities/FNextPushId.h
@@ -0,0 +1,23 @@
+/*
+ * 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 FNextPushId : NSObject
+
++ (NSString *) get:(NSTimeInterval)now;
+
+@end
diff --git a/Firebase/Database/Utilities/FNextPushId.m b/Firebase/Database/Utilities/FNextPushId.m
new file mode 100644
index 0000000..af54e3d
--- /dev/null
+++ b/Firebase/Database/Utilities/FNextPushId.m
@@ -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 "FNextPushId.h"
+#import "FUtilities.h"
+
+static NSString *const PUSH_CHARS = @"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
+
+@implementation FNextPushId
+
++ (NSString *) get:(NSTimeInterval)currentTime {
+ static long long lastPushTime = 0;
+ static int lastRandChars[12];
+
+ long long now = (long long)(currentTime * 1000);
+
+ BOOL duplicateTime = now == lastPushTime;
+ lastPushTime = now;
+
+ unichar timeStampChars[8];
+ for(int i = 7; i >= 0; i--) {
+ timeStampChars[i] = [PUSH_CHARS characterAtIndex:(now % 64)];
+ now = (long long)floor(now / 64);
+ }
+
+ NSMutableString* id = [[NSMutableString alloc] init];
+ [id appendString:[NSString stringWithCharacters:timeStampChars length:8]];
+
+
+ if(!duplicateTime) {
+ for(int i = 0; i < 12; i++) {
+ lastRandChars[i] = (int)floor(arc4random() % 64);
+ }
+ }
+ else {
+ int i = 0;
+ for(i = 11; i >= 0 && lastRandChars[i] == 63; i--) {
+ lastRandChars[i] = 0;
+ }
+ lastRandChars[i]++;
+ }
+
+ for(int i = 0; i < 12; i++) {
+ [id appendFormat:@"%C", [PUSH_CHARS characterAtIndex:lastRandChars[i]]];
+ }
+
+ return [NSString stringWithString:id];
+}
+
+@end
diff --git a/Firebase/Database/Utilities/FParsedUrl.h b/Firebase/Database/Utilities/FParsedUrl.h
new file mode 100644
index 0000000..7145f86
--- /dev/null
+++ b/Firebase/Database/Utilities/FParsedUrl.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 "FRepoInfo.h"
+#import "FPath.h"
+
+@interface FParsedUrl : NSObject
+
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+@property (nonatomic, strong) FPath* path;
+
+@end
diff --git a/Firebase/Database/Utilities/FParsedUrl.m b/Firebase/Database/Utilities/FParsedUrl.m
new file mode 100644
index 0000000..eb83330
--- /dev/null
+++ b/Firebase/Database/Utilities/FParsedUrl.m
@@ -0,0 +1,24 @@
+/*
+ * 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 "FParsedUrl.h"
+
+@implementation FParsedUrl
+
+@synthesize repoInfo;
+@synthesize path;
+
+@end
diff --git a/Firebase/Database/Utilities/FStringUtilities.h b/Firebase/Database/Utilities/FStringUtilities.h
new file mode 100644
index 0000000..34ac9af
--- /dev/null
+++ b/Firebase/Database/Utilities/FStringUtilities.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>
+
+@interface FStringUtilities : NSObject
+
++ (NSString *) base64EncodedSha1:(NSString *)str;
++ (NSString *) urlDecoded:(NSString *)url;
++ (NSString *) urlEncoded:(NSString *)url;
++ (NSString *) sanitizedForUserAgent:(NSString *)str;
+
+@end
diff --git a/Firebase/Database/Utilities/FStringUtilities.m b/Firebase/Database/Utilities/FStringUtilities.m
new file mode 100644
index 0000000..dff58e0
--- /dev/null
+++ b/Firebase/Database/Utilities/FStringUtilities.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 <CommonCrypto/CommonDigest.h>
+#import "FStringUtilities.h"
+#import "NSData+SRB64Additions.h"
+
+@implementation FStringUtilities
+
+// http://stackoverflow.com/questions/3468268/objective-c-sha1
+// http://stackoverflow.com/questions/7310457/ios-objective-c-sha-1-and-base64-problem
++ (NSString *) base64EncodedSha1:(NSString *)str {
+ const char *cstr = [str cStringUsingEncoding:NSUTF8StringEncoding];
+ // NSString reports length in characters, but we want it in bytes, which strlen will give us.
+ unsigned long dataLen = strlen(cstr);
+ NSData *data = [NSData dataWithBytes:cstr length:dataLen];
+ uint8_t digest[CC_SHA1_DIGEST_LENGTH];
+ CC_SHA1(data.bytes, (unsigned int)data.length, digest);
+ NSData* output = [[NSData alloc] initWithBytes:digest length:CC_SHA1_DIGEST_LENGTH];
+ return [FSRUtilities base64EncodedStringFromData:output];
+}
+
++ (NSString *) urlDecoded:(NSString *)url {
+ NSString* replaced = [url stringByReplacingOccurrencesOfString:@"+" withString:@" "];
+ NSString* decoded = [replaced stringByRemovingPercentEncoding];
+ // This is kind of a hack, but is generally how the js client works. We could run into trouble if
+ // some piece is a correctly escaped %-sequence, and another isn't. But, that's bad input anyways...
+ if (decoded) {
+ return decoded;
+ } else {
+ return replaced;
+ }
+}
+
++ (NSString *) urlEncoded:(NSString *)url {
+ // Didn't seem like there was an Apple NSCharacterSet that had our version of the encoding
+ // So I made my own, following RFC 2396 https://www.ietf.org/rfc/rfc2396.txt
+ // allowedCharacters = alphanum | "-" | "_" | "~"
+ NSCharacterSet *allowedCharacters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~"];
+ return [url stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
+}
+
++ (NSString *) sanitizedForUserAgent:(NSString *)str {
+ return [str stringByReplacingOccurrencesOfString:@"/|_" withString:@"|" options:NSRegularExpressionSearch range:NSMakeRange(0, [str length])];
+}
+
+
+@end
diff --git a/Firebase/Database/Utilities/FTypedefs.h b/Firebase/Database/Utilities/FTypedefs.h
new file mode 100644
index 0000000..4a24ca5
--- /dev/null
+++ b/Firebase/Database/Utilities/FTypedefs.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>
+
+#ifndef Firebase_FTypedefs_h
+#define Firebase_FTypedefs_h
+
+/**
+ * Stub...
+ */
+@class FIRDataSnapshot;
+@class FIRDatabaseReference;
+@class FAuthData;
+@protocol FNode;
+
+// fbt = Firebase Block Typedef
+
+typedef void (^fbt_void_void)(void);
+typedef void (^fbt_void_datasnapshot_nsstring) (FIRDataSnapshot *snapshot, NSString *prevName);
+typedef void (^fbt_void_datasnapshot) (FIRDataSnapshot *snapshot);
+typedef void (^fbt_void_user)(FAuthData *user);
+typedef void (^fbt_void_nsstring_id)(NSString* status, id data);
+typedef void (^fbt_void_nserror_id)(NSError* error, id data);
+typedef void (^fbt_void_nserror)(NSError *error);
+typedef void (^fbt_void_nserror_ref)(NSError* error, FIRDatabaseReference * ref);
+typedef void (^fbt_void_nserror_user)(NSError* error, FAuthData * user);
+typedef void (^fbt_void_nserror_json)(NSError* error, NSDictionary* json);
+typedef void (^fbt_void_nsdictionary)(NSDictionary *data);
+typedef id (^fbt_id_node_nsstring)(id<FNode> node, NSString* childName);
+
+#endif
diff --git a/Firebase/Database/Utilities/FUtilities.h b/Firebase/Database/Utilities/FUtilities.h
new file mode 100644
index 0000000..f5e312f
--- /dev/null
+++ b/Firebase/Database/Utilities/FUtilities.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 "FIRLogger.h"
+#import "FParsedUrl.h"
+
+@interface FUtilities : NSObject
+
++ (NSArray *) splitString:(NSString *)str intoMaxSize:(const unsigned int)size;
++ (NSNumber *) LUIDGenerator;
++ (FParsedUrl *) parseUrl:(NSString *)url;
++ (NSString *) getJavascriptType:(id)obj;
++ (NSError *) errorForStatus:(NSString *)status andReason:(NSString *)reason;
++ (NSNumber *) intForString:(NSString *)string;
++ (NSString *) ieee754StringForNumber:(NSNumber *)val;
++ (void) setLoggingEnabled:(BOOL)enabled;
++ (BOOL) getLoggingEnabled;
+
++ (NSString*) minName;
++ (NSString*) maxName;
++ (NSComparisonResult) compareKey:(NSString *)a toKey:(NSString *)b;
++ (NSComparator) stringComparator;
++ (NSComparator) keyComparator;
+
++ (double)randomDouble;
+
+@end
+
+typedef enum {
+ FLogLevelDebug = 1,
+ FLogLevelInfo = 2,
+ FLogLevelWarn = 3,
+ FLogLevelError = 4,
+ FLogLevelNone = 5
+} FLogLevel;
+
+// Log tags
+FOUNDATION_EXPORT NSString *const kFPersistenceLogTag;
+
+#define FFLog(code, format, ...) FFDebug((code), (format), ##__VA_ARGS__)
+
+#define FFDebug(code, format, ...) do { \
+ if (FFIsLoggingEnabled(FLogLevelDebug)) { \
+ FIRLogDebug(kFIRLoggerDatabase, (code), (format), ##__VA_ARGS__); \
+ } \
+} while(0)
+
+#define FFInfo(code, format, ...) do { \
+ if (FFIsLoggingEnabled(FLogLevelInfo)) { \
+ FIRLogError(kFIRLoggerDatabase, (code), (format), ##__VA_ARGS__); \
+ } \
+} while(0)
+
+#define FFWarn(code, format, ...) do { \
+ if (FFIsLoggingEnabled(FLogLevelWarn)) { \
+ FIRLogWarning(kFIRLoggerDatabase, (code), (format), ##__VA_ARGS__); \
+ } \
+} while(0)
+
+BOOL FFIsLoggingEnabled(FLogLevel logLevel);
+void firebaseUncaughtExceptionHandler(NSException *exception);
+void firebaseJobsTroll(void);
diff --git a/Firebase/Database/Utilities/FUtilities.m b/Firebase/Database/Utilities/FUtilities.m
new file mode 100644
index 0000000..7c25e3b
--- /dev/null
+++ b/Firebase/Database/Utilities/FUtilities.m
@@ -0,0 +1,389 @@
+/*
+ * 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 "FUtilities.h"
+#import "FStringUtilities.h"
+#import "FConstants.h"
+#import "FAtomicNumber.h"
+
+#define ARC4RANDOM_MAX 0x100000000
+#define INTEGER_32_MIN (-2147483648)
+#define INTEGER_32_MAX 2147483647
+
+#pragma mark -
+#pragma mark C functions
+
+static FLogLevel logLevel = FLogLevelInfo; // Default log level is info
+static NSMutableDictionary* options = nil;
+
+BOOL FFIsLoggingEnabled(FLogLevel level) {
+ return level >= logLevel;
+}
+
+void firebaseJobsTroll(void) {
+ FFLog(@"I-RDB095001", @"password super secret; JFK conspiracy; Hello there! Having fun digging through Firebase? We're always hiring! jobs@firebase.com");
+}
+
+#pragma mark -
+#pragma mark Private property and singleton specification
+
+@interface FUtilities() {
+
+}
+
+@property (nonatomic, strong) FAtomicNumber* localUid;
+
++ (FUtilities*)singleton;
+
+@end
+
+@implementation FUtilities
+
+@synthesize localUid;
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ self.localUid = [[FAtomicNumber alloc] init];
+ }
+ return self;
+}
+
+// TODO: We really want to be able to set the log level
++ (void) setLoggingEnabled:(BOOL)enabled {
+ logLevel = enabled ? FLogLevelDebug : FLogLevelInfo;
+}
+
++ (BOOL) getLoggingEnabled {
+ return logLevel == FLogLevelDebug;
+}
+
++ (FUtilities*) singleton
+{
+ static dispatch_once_t pred = 0;
+ __strong static id _sharedObject = nil;
+ dispatch_once(&pred, ^{
+ _sharedObject = [[self alloc] init]; // or some other init method
+ });
+ return _sharedObject;
+}
+
+// Refactor as a category of NSString
++ (NSArray *) splitString:(NSString *) str intoMaxSize:(const unsigned int) size {
+ if(str.length <= size) {
+ return [NSArray arrayWithObject:str];
+ }
+
+ NSMutableArray* dataSegs = [[NSMutableArray alloc] init];
+ for(int c = 0; c < str.length; c += size) {
+ if (c + size > str.length) {
+ int rangeStart = c;
+ unsigned long rangeLength = size - ((c + size) - str.length);
+ [dataSegs addObject:[str substringWithRange:NSMakeRange(rangeStart, rangeLength)]];
+ }
+ else {
+ int rangeStart = c;
+ int rangeLength = size;
+ [dataSegs addObject:[str substringWithRange:NSMakeRange(rangeStart, rangeLength)]];
+ }
+ }
+ return dataSegs;
+}
+
++ (NSNumber *) LUIDGenerator {
+ FUtilities* f = [FUtilities singleton];
+ return [f.localUid getAndIncrement];
+}
+
++ (NSString *) decodePath:(NSString *)pathString {
+ NSMutableArray* decodedPieces = [[NSMutableArray alloc] init];
+ NSArray* pieces = [pathString componentsSeparatedByString:@"/"];
+ for (NSString* piece in pieces) {
+ if (piece.length > 0) {
+ [decodedPieces addObject:[FStringUtilities urlDecoded:piece]];
+ }
+ }
+ return [NSString stringWithFormat:@"/%@", [decodedPieces componentsJoinedByString:@"/"]];
+}
+
++ (FParsedUrl *) parseUrl:(NSString *)url {
+ NSString* original = url;
+ //NSURL* n = [[NSURL alloc] initWithString:url]
+
+ NSString* host;
+ NSString* namespace;
+ bool secure;
+
+ NSString* scheme = nil;
+ FPath* path = nil;
+ NSRange colonIndex = [url rangeOfString:@"//"];
+ if (colonIndex.location != NSNotFound) {
+ scheme = [url substringToIndex:colonIndex.location - 1];
+ url = [url substringFromIndex:colonIndex.location + 2];
+ }
+ NSInteger slashIndex = [url rangeOfString:@"/"].location;
+ if (slashIndex == NSNotFound) {
+ slashIndex = url.length;
+ }
+
+ host = [[url substringToIndex:slashIndex] lowercaseString];
+ if (slashIndex >= url.length) {
+ url = @"";
+ } else {
+ url = [url substringFromIndex:slashIndex + 1];
+ }
+
+ NSArray *parts = [host componentsSeparatedByString:@"."];
+ if([parts count] == 3) {
+ NSInteger colonIndex = [[parts objectAtIndex:2] rangeOfString:@":"].location;
+ if (colonIndex != NSNotFound) {
+ // we have a port, use the provided scheme
+ secure = [scheme isEqualToString:@"https"];
+ } else {
+ secure = YES;
+ }
+
+ namespace = [[parts objectAtIndex:0] lowercaseString];
+ NSString* pathString = [self decodePath:[NSString stringWithFormat:@"/%@", url]];
+ path = [[FPath alloc] initWith:pathString];
+ }
+ else {
+ [NSException raise:@"No Firebase database specified." format:@"No Firebase database found for input: %@", url];
+ }
+
+ FRepoInfo* repoInfo = [[FRepoInfo alloc] initWithHost:host isSecure:secure withNamespace:namespace];
+
+ FFLog(@"I-RDB095002", @"---> Parsed (%@) to: (%@,%@); ns=(%@); path=(%@)", original, [repoInfo description], [repoInfo connectionURL], repoInfo.namespace, [path description]);
+
+ FParsedUrl* parsedUrl = [[FParsedUrl alloc] init];
+ parsedUrl.repoInfo = repoInfo;
+ parsedUrl.path = path;
+
+ return parsedUrl;
+}
+
+/*
+ case str: JString => priString + "string:" + str.s;
+ case bool: JBool => priString + "boolean:" + bool.value;
+ case double: JDouble => priString + "number:" + double.num;
+ case int: JInt => priString + "number:" + int.num;
+ case _ => {
+ error("Leaf node has value '" + data.value + "' of invalid type '" + data.value.getClass.toString + "'");
+ "";
+ }
+ */
+
++ (NSString *) getJavascriptType:(id)obj {
+ if ([obj isKindOfClass:[NSDictionary class]]) {
+ return kJavaScriptObject;
+ } else if([obj isKindOfClass:[NSString class]]) {
+ return kJavaScriptString;
+ }
+ else if ([obj isKindOfClass:[NSNumber class]]) {
+ // We used to just compare to @encode(BOOL) as suggested at
+ // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber, but on arm64, @encode(BOOL) returns "B"
+ // instead of "c" even though objCType still returns 'c' (signed char). So check both.
+ if(strcmp([obj objCType], @encode(BOOL)) == 0 ||
+ strcmp([obj objCType], @encode(signed char)) == 0) {
+ return kJavaScriptBoolean;
+ }
+ else {
+ return kJavaScriptNumber;
+ }
+ }
+ else {
+ return kJavaScriptNull;
+ }
+}
+
++ (NSError *) errorForStatus:(NSString *)status andReason:(NSString *)reason {
+ static dispatch_once_t pred = 0;
+ __strong static NSDictionary* errorMap = nil;
+ __strong static NSDictionary* errorCodes = nil;
+ dispatch_once(&pred, ^{
+ errorMap = @{
+ @"permission_denied": @"Permission Denied",
+ @"unavailable": @"Service is unavailable",
+ kFErrorWriteCanceled: @"Write cancelled by user"
+ };
+ errorCodes = @{
+ @"permission_denied": @1,
+ @"unavailable": @2,
+ kFErrorWriteCanceled: @3
+ };
+ });
+
+ if ([status isEqualToString:kFWPResponseForActionStatusOk]) {
+ return nil;
+ } else {
+ NSInteger code;
+ NSString* desc = nil;
+ if (reason) {
+ desc = reason;
+ } else if ([errorMap objectForKey:status] != nil) {
+ desc = [errorMap objectForKey:status];
+ } else {
+ desc = status;
+ }
+
+ if ([errorCodes objectForKey:status] != nil) {
+ NSNumber* num = [errorCodes objectForKey:status];
+ code = [num integerValue];
+ } else {
+ // XXX what to do here?
+ code = 9999;
+ }
+
+ return [[NSError alloc] initWithDomain:kFErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: desc}];
+ }
+}
+
++ (NSNumber *) intForString:(NSString *)string {
+ static NSCharacterSet *notDigits = nil;
+ if (!notDigits) {
+ notDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
+ }
+ if ([string rangeOfCharacterFromSet:notDigits].length == 0) {
+ NSInteger num;
+ NSScanner* scanner = [NSScanner scannerWithString:string];
+ if ([scanner scanInteger:&num]) {
+ return [NSNumber numberWithInteger:num];
+ }
+ }
+ return nil;
+}
+
++ (NSString *) ieee754StringForNumber:(NSNumber *)val {
+ double d = [val doubleValue];
+ NSData* data = [NSData dataWithBytes:&d length:sizeof(double)];
+ NSMutableString* str = [[NSMutableString alloc] init];
+ const unsigned char* buffer = (const unsigned char*)[data bytes];
+ for (int i = 0; i < data.length; i++) {
+ unsigned char byte = buffer[7 - i];
+ [str appendFormat:@"%02x", byte];
+ }
+ return str;
+}
+
+static inline BOOL tryParseStringToInt(__unsafe_unretained NSString* str, NSInteger* integer) {
+ // First do some cheap checks (NOTE: The below checks are significantly faster than an equivalent regex :-( ).
+ NSUInteger length = str.length;
+ if (length > 11 || length == 0) {
+ return NO;
+ }
+ long long value = 0;
+ BOOL negative = NO;
+ NSUInteger i = 0;
+ if ([str characterAtIndex:0] == '-') {
+ if (length == 1) {
+ return NO;
+ }
+ negative = YES;
+ i = 1;
+ }
+ for(; i < length; i++) {
+ unichar c = [str characterAtIndex:i];
+ // Must be a digit, or '-' if it's the first char.
+ if (c < '0' || c > '9') {
+ return NO;
+ } else {
+ int charValue = c - '0';
+ value = value*10 + charValue;
+ }
+ }
+
+ value = (negative) ? -value : value;
+
+ if (value < INTEGER_32_MIN || value > INTEGER_32_MAX) {
+ return NO;
+ } else {
+ *integer = (NSInteger)value;
+ return YES;
+ }
+}
+
++ (NSString *) maxName {
+ static dispatch_once_t once;
+ static NSString *maxName;
+ dispatch_once(&once, ^{
+ maxName = [[NSString alloc] initWithFormat:@"[MAX_NAME]"];
+ });
+ return maxName;
+}
+
++ (NSString *) minName {
+ static dispatch_once_t once;
+ static NSString *minName;
+ dispatch_once(&once, ^{
+ minName = [[NSString alloc] initWithFormat:@"[MIN_NAME]"];
+ });
+ return minName;
+}
+
++ (NSComparisonResult) compareKey:(NSString *)a toKey:(NSString *)b {
+ if (a == b) {
+ return NSOrderedSame;
+ } else if (a == [FUtilities minName] || b == [FUtilities maxName]) {
+ return NSOrderedAscending;
+ } else if (b == [FUtilities minName] || a == [FUtilities maxName]) {
+ return NSOrderedDescending;
+ } else {
+ NSInteger aAsInt, bAsInt;
+ if (tryParseStringToInt(a, &aAsInt)) {
+ if (tryParseStringToInt(b, &bAsInt)) {
+ if (aAsInt > bAsInt) {
+ return NSOrderedDescending;
+ } else if (aAsInt < bAsInt) {
+ return NSOrderedAscending;
+ } else if (a.length > b.length) {
+ return NSOrderedDescending;
+ } else if (a.length < b.length) {
+ return NSOrderedAscending;
+ } else {
+ return NSOrderedSame;
+ }
+ } else {
+ return (NSComparisonResult) NSOrderedAscending;
+ }
+ } else if (tryParseStringToInt(b, &bAsInt)) {
+ return (NSComparisonResult) NSOrderedDescending;
+ } else {
+ // Perform literal character by character search to prevent a > b && b > a issues.
+ // Note that calling -(NSString *)decomposedStringWithCanonicalMapping also works.
+ return [a compare:b options:NSLiteralSearch];
+ }
+ }
+}
+
++ (NSComparator) keyComparator {
+ return ^NSComparisonResult(__unsafe_unretained NSString *a, __unsafe_unretained NSString *b) {
+ return [FUtilities compareKey:a toKey:b];
+ };
+}
+
++ (NSComparator) stringComparator {
+ return ^NSComparisonResult(__unsafe_unretained NSString *a, __unsafe_unretained NSString *b) {
+ return [a compare:b];
+ };
+}
+
++ (double) randomDouble {
+ return ((double) arc4random() / ARC4RANDOM_MAX);
+}
+
+@end
+
diff --git a/Firebase/Database/Utilities/FValidation.h b/Firebase/Database/Utilities/FValidation.h
new file mode 100644
index 0000000..faa8f76
--- /dev/null
+++ b/Firebase/Database/Utilities/FValidation.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>
+#import "FPath.h"
+#import "FIRDataEventType.h"
+#import "FParsedUrl.h"
+#import "FTypedefs.h"
+
+@interface FValidation : NSObject
+
++ (void) validateFrom:(NSString *)fn writablePath:(FPath *)path;
++ (void) validateFrom:(NSString *)fn knownEventType:(FIRDataEventType)event;
++ (void) validateFrom:(NSString *)fn validPathString:(NSString *)pathString;
++ (void) validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString;
++ (void) validateFrom:(NSString *)fn validKey:(NSString *)key;
++ (void) validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl;
+
++ (void) validateToken:(NSString *)token;
+
+// Functions for handling passing errors back
++ (void) handleError:(NSError *)error withUserCallback:(fbt_void_nserror_id)userCallback;
++ (void) handleError:(NSError *)error withSuccessCallback:(fbt_void_nserror)userCallback;
+
+// Functions used for validating while creating snapshots in FSnapshotUtilities
++ (BOOL) validateFrom:(NSString*)fn isValidLeafValue:(id)value withPath:(NSArray*)path;
++ (void) validateFrom:(NSString*)fn validDictionaryKey:(id)keyId withPath:(NSArray*)path;
++ (void) validateFrom:(NSString*)fn validUpdateDictionaryKey:(id)keyId withValue:(id)value;
++ (void) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path;
++ (BOOL) validatePriorityValue:value;
+
+@end
diff --git a/Firebase/Database/Utilities/FValidation.m b/Firebase/Database/Utilities/FValidation.m
new file mode 100644
index 0000000..c4c6b2b
--- /dev/null
+++ b/Firebase/Database/Utilities/FValidation.m
@@ -0,0 +1,312 @@
+/*
+ * 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 "FValidation.h"
+#import "FConstants.h"
+#import "FParsedUrl.h"
+#import "FTypedefs.h"
+
+
+// Have to escape: * ? + [ ( ) { } ^ $ | \ . /
+// See: https://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSRegularExpression_Class/Reference/Reference.html
+
+NSString *const kInvalidPathCharacters = @"[].#$";
+NSString *const kInvalidKeyCharacters = @"[].#$/";
+
+@implementation FValidation
+
++ (void) validateFrom:(NSString *)fn writablePath:(FPath *)path {
+ if([[path getFront] isEqualToString:kDotInfoPrefix]) {
+ @throw [[NSException alloc] initWithName:@"WritablePathValidation" reason:[NSString stringWithFormat:@"(%@) failed to path %@: Can't modify data under %@", fn, [path description], kDotInfoPrefix] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString*)fn knownEventType:(FIRDataEventType)event {
+ switch (event) {
+ case FIRDataEventTypeValue:
+ case FIRDataEventTypeChildAdded:
+ case FIRDataEventTypeChildChanged:
+ case FIRDataEventTypeChildMoved:
+ case FIRDataEventTypeChildRemoved:
+ return;
+ break;
+ default:
+ @throw [[NSException alloc] initWithName:@"KnownEventTypeValidation" reason:[NSString stringWithFormat:@"(%@) Unknown event type: %d", fn, (int) event] userInfo:nil];
+ break;
+ }
+}
+
++ (BOOL) isValidPathString:(NSString *)pathString {
+ static dispatch_once_t token;
+ static NSCharacterSet *badPathChars = nil;
+ dispatch_once(&token, ^{
+ badPathChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidPathCharacters];
+ });
+ return pathString != nil && [pathString length] != 0 &&
+ [pathString rangeOfCharacterFromSet:badPathChars].location == NSNotFound;
+}
+
++ (void) validateFrom:(NSString *)fn validPathString:(NSString *)pathString {
+ if(! [self isValidPathString:pathString]) {
+ @throw [[NSException alloc] initWithName:@"InvalidPathValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString *)fn validRootPathString:(NSString *)pathString {
+ static dispatch_once_t token;
+ static NSRegularExpression *dotInfoRegex = nil;
+ dispatch_once(&token, ^{
+ dotInfoRegex = [NSRegularExpression regularExpressionWithPattern:@"^\\/*\\.info(\\/|$)" options:0 error:nil];
+ });
+
+ NSString *tempPath = pathString;
+ // HACK: Obj-C regex are kinda' slow. Do a plain string search first before bothering with the regex.
+ if ([pathString rangeOfString:@".info"].location != NSNotFound) {
+ tempPath = [dotInfoRegex stringByReplacingMatchesInString:pathString options:0 range:NSMakeRange(0, pathString.length) withTemplate:@"/"];
+ }
+ [self validateFrom:fn validPathString:tempPath];
+}
+
++ (BOOL) isValidKey:(NSString *)key {
+ static dispatch_once_t token;
+ static NSCharacterSet *badKeyChars = nil;
+ dispatch_once(&token, ^{
+ badKeyChars = [NSCharacterSet characterSetWithCharactersInString:kInvalidKeyCharacters];
+ });
+ return key != nil && key.length > 0 && [key rangeOfCharacterFromSet:badKeyChars].location == NSNotFound;
+}
+
++ (void) validateFrom:(NSString *)fn validKey:(NSString *)key {
+ if (![self isValidKey:key]) {
+ @throw [[NSException alloc] initWithName:@"InvalidKeyValidation" reason:[NSString stringWithFormat:@"(%@) Must be a non-empty string and not contain '/' '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString *)fn validURL:(FParsedUrl *)parsedUrl {
+ NSString* pathString = [parsedUrl.path description];
+ [self validateFrom:fn validRootPathString:pathString];
+}
+
+#pragma mark -
+#pragma mark Authentication validation
+
++ (BOOL) stringNonempty:(NSString *)str {
+ return str != nil && ![str isKindOfClass:[NSNull class]] && str.length > 0;
+}
+
++ (void) validateToken:(NSString *)token {
+ if (![FValidation stringNonempty:token]) {
+ [NSException raise:NSInvalidArgumentException format:@"Can't have empty string or nil for custom token"];
+ }
+}
+
+#pragma mark -
+#pragma mark Handling authentication errors
+
+/**
+* This function immediately calls the callback.
+* It assumes that it is not on FirebaseWorker thread.
+* It assumes it's on a user-controlled thread.
+*/
++ (void) handleError:(NSError *)error withUserCallback:(fbt_void_nserror_id)userCallback {
+ if (userCallback) {
+ userCallback(error, nil);
+ }
+}
+
+/**
+* This function immediately calls the callback.
+* It assumes that it is not on FirebaseWorker thread.
+* It assumes it's on a user-controlled thread.
+*/
++ (void) handleError:(NSError *)error withSuccessCallback:(fbt_void_nserror)userCallback {
+ if (userCallback) {
+ userCallback(error);
+ }
+}
+
+#pragma mark -
+#pragma mark Snapshot validation
+
++ (BOOL) validateFrom:(NSString*)fn isValidLeafValue:(id)value withPath:(NSArray*)path {
+ if ([value isKindOfClass:[NSString class]]) {
+ // Try to avoid conversion to bytes if possible
+ NSString* theString = value;
+ if ([theString maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize &&
+ [theString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kFirebaseMaxLeafSize) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) String exceeds max size of %u utf8 bytes: %@", fn, (int)kFirebaseMaxLeafSize, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+
+ else if ([value isKindOfClass:[NSNumber class]]) {
+ // Cannot store NaN, but otherwise can store NSNumbers.
+ if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN at path: %@.", fn, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+
+ else if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary* dval = value;
+ if (dval[kServerValueSubKey] != nil) {
+ if ([dval count] > 1) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys.%@.", fn, pathString] userInfo:nil];
+ }
+ return YES;
+ }
+ return NO;
+ }
+
+ else if (value == [NSNull null] || value == nil) {
+ // Null is valid type to store at leaf
+ return YES;
+ }
+
+ return NO;
+}
+
++ (NSString*) parseAndValidateKey:(id)keyId fromFunction:(NSString*)fn path:(NSArray*)path {
+ if (![keyId isKindOfClass:[NSString class]]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString* pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Non-string keys are not allowed in object at path: %@", fn, pathString] userInfo:nil];
+ }
+ return (NSString*)keyId;
+}
+
++ (void) validateFrom:(NSString*)fn validDictionaryKey:(id)keyId withPath:(NSArray*)path {
+ NSString *key = [self parseAndValidateKey:keyId fromFunction:fn path:path];
+ if (![key isEqualToString:kPayloadPriority] && ![key isEqualToString:kPayloadValue] && ![key isEqualToString:kServerValueSubKey] && ![FValidation isValidKey:key]) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object at path: %@. Keys must be non-empty and cannot contain '/' '.' '#' '$' '[' or ']'", fn, pathString] userInfo:nil];
+ }
+}
+
++ (void) validateFrom:(NSString*)fn validUpdateDictionaryKey:(id)keyId withValue:(id)value {
+ FPath *path = [FPath pathWithString:[self parseAndValidateKey:keyId fromFunction:fn path:@[]]];
+ __block NSInteger keyNum = 0;
+ [path enumerateComponentsUsingBlock:^void (NSString *key, BOOL *stop) {
+ if ([key isEqualToString:kPayloadPriority] && keyNum == [path length] - 1) {
+ [self validateFrom:fn isValidPriorityValue:value withPath:@[]];
+ } else {
+ keyNum++;
+
+ if (![FValidation isValidKey:key]) {
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Invalid key in object. Keys must be non-empty and cannot contain '.' '#' '$' '[' or ']'", fn] userInfo:nil];
+ }
+ }
+ }];
+}
+
++ (void) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path {
+ [self validateFrom:fn isValidPriorityValue:value withPath:path throwError:YES];
+}
+
+/**
+* Returns YES if priority is valid.
+*/
++ (BOOL)validatePriorityValue:value {
+ return [self validateFrom:nil isValidPriorityValue:value withPath:nil throwError:NO];
+}
+
+/**
+* Helper for validating priorities. If passed YES for throwError, it'll throw descriptive errors on validation
+* problems. Else, it'll just return YES/NO.
+*/
++ (BOOL) validateFrom:(NSString*)fn isValidPriorityValue:(id)value withPath:(NSArray*)path throwError:(BOOL)throwError {
+ if ([value isKindOfClass:[NSNumber class]]) {
+ if ([[NSDecimalNumber notANumber] isEqualToNumber:value]) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store NaN as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ } else if (value == (id) kCFBooleanFalse || value == (id) kCFBooleanTrue) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store true/false as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ }
+ else if ([value isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *dval = value;
+ if (dval[kServerValueSubKey] != nil) {
+ if ([dval count] > 1) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store other keys with server value keys as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ } else {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSDictionary as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+ }
+ else if ([value isKindOfClass:[NSArray class]]) {
+ if (throwError) {
+ NSRange range;
+ range.location = 0;
+ range.length = MIN(path.count, 50);
+ NSString *pathString = [[path subarrayWithRange:range] componentsJoinedByString:@"."];
+ @throw [[NSException alloc] initWithName:@"InvalidFirebaseData" reason:[NSString stringWithFormat:@"(%@) Cannot store an NSArray as priority at path: %@.", fn, pathString] userInfo:nil];
+ } else {
+ return NO;
+ }
+ }
+
+ // It's valid!
+ return YES;
+}
+@end
diff --git a/Firebase/Database/Utilities/NSString+FURLUtils.h b/Firebase/Database/Utilities/NSString+FURLUtils.h
new file mode 100644
index 0000000..7bd39bc
--- /dev/null
+++ b/Firebase/Database/Utilities/NSString+FURLUtils.h
@@ -0,0 +1,24 @@
+/*
+ * 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 NSString (FURLUtils)
+
+- (NSString *) urlEncoded;
+- (NSString *) urlDecoded;
+
+@end
diff --git a/Firebase/Database/Utilities/NSString+FURLUtils.m b/Firebase/Database/Utilities/NSString+FURLUtils.m
new file mode 100644
index 0000000..2e018c8
--- /dev/null
+++ b/Firebase/Database/Utilities/NSString+FURLUtils.m
@@ -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 "NSString+FURLUtils.h"
+
+@implementation NSString (FURLUtils)
+
+- (NSString *) urlDecoded {
+ NSString* replaced = [self stringByReplacingOccurrencesOfString:@"+" withString:@" "];
+ NSString* decoded = [replaced stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ // This is kind of a hack, but is generally how the js client works. We could run into trouble if
+ // some piece is a correctly escaped %-sequence, and another isn't. But, that's bad input anyways...
+ if (decoded) {
+ return decoded;
+ } else {
+ return replaced;
+ }
+}
+
+- (NSString *) urlEncoded {
+ CFStringRef urlString = CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)self, NULL, (CFStringRef)@"!*'\"();:@&=+$,/?%#[]% ", kCFStringEncodingUTF8);
+ return (__bridge NSString *) urlString;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.h
new file mode 100644
index 0000000..bceeed2
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.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>
+#import "FTypedefs.h"
+
+@interface FTupleBoolBlock : NSObject
+
+@property (nonatomic, readwrite) BOOL boolean;
+@property (nonatomic, copy) fbt_void_void block;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m
new file mode 100644
index 0000000..c4cd8bf
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleBoolBlock.m
@@ -0,0 +1,24 @@
+/*
+ * 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 "FTupleBoolBlock.h"
+
+@implementation FTupleBoolBlock
+
+@synthesize boolean;
+@synthesize block;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h
new file mode 100644
index 0000000..6ec2375
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "FTypedefs_Private.h"
+
+@interface FTupleCallbackStatus : NSObject
+@property (nonatomic, copy) fbt_void_nsstring_nsstring block;
+@property (nonatomic) NSString* status;
+@property (nonatomic) NSString* errorReason;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m
new file mode 100644
index 0000000..05914bf
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleCallbackStatus.m
@@ -0,0 +1,22 @@
+/*
+ * 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 "FTupleCallbackStatus.h"
+
+@implementation FTupleCallbackStatus
+@synthesize block;
+@synthesize status;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleFirebase.h b/Firebase/Database/Utilities/Tuples/FTupleFirebase.h
new file mode 100644
index 0000000..ff84bbb
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleFirebase.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 "FIRDatabaseReference.h"
+
+@interface FTupleFirebase : NSObject
+
+@property (nonatomic, strong) FIRDatabaseReference * one;
+@property (nonatomic, strong) FIRDatabaseReference * two;
+@property (nonatomic, strong) FIRDatabaseReference * three;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleFirebase.m b/Firebase/Database/Utilities/Tuples/FTupleFirebase.m
new file mode 100644
index 0000000..3956f8b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleFirebase.m
@@ -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 "FTupleFirebase.h"
+
+@implementation FTupleFirebase
+
+@synthesize one;
+@synthesize two;
+@synthesize three;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleNodePath.h b/Firebase/Database/Utilities/Tuples/FTupleNodePath.h
new file mode 100644
index 0000000..fbf62c7
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleNodePath.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 "FPath.h"
+#import "FNode.h"
+
+@interface FTupleNodePath : NSObject
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, strong) id<FNode> node;
+
+- (id) initWithNode:(id<FNode>)aNode andPath:(FPath *)aPath;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleNodePath.m b/Firebase/Database/Utilities/Tuples/FTupleNodePath.m
new file mode 100644
index 0000000..eefc0c2
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleNodePath.m
@@ -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 "FTupleNodePath.h"
+
+@implementation FTupleNodePath
+
+@synthesize path;
+@synthesize node;
+
+- (id) initWithNode:(id<FNode>)aNode andPath:(FPath *)aPath {
+ self = [super init];
+ if (self) {
+ self.path = aPath;
+ self.node = aNode;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjectNode.h b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.h
new file mode 100644
index 0000000..6fcb746
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.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 FTupleObjectNode : NSObject
+
+- (id)initWithObject:(id)aObj andNode:(id<FNode>)aNode;
+
+@property (nonatomic, strong) id<FNode> node;
+@property (nonatomic, strong) id obj;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjectNode.m b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.m
new file mode 100644
index 0000000..4c533b0
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjectNode.m
@@ -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 "FTupleObjectNode.h"
+
+@implementation FTupleObjectNode
+
+@synthesize obj;
+@synthesize node;
+
+- (id)initWithObject:(id)aObj andNode:(id<FNode>)aNode {
+ self = [super init];
+ if (self) {
+ self.obj = aObj;
+ self.node = aNode;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjects.h b/Firebase/Database/Utilities/Tuples/FTupleObjects.h
new file mode 100644
index 0000000..4ff1fcf
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjects.h
@@ -0,0 +1,24 @@
+/*
+ * 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 FTupleObjects : NSObject
+
+@property (nonatomic, strong) id objA;
+@property (nonatomic, strong) id objB;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleObjects.m b/Firebase/Database/Utilities/Tuples/FTupleObjects.m
new file mode 100644
index 0000000..a9e4c88
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleObjects.m
@@ -0,0 +1,24 @@
+/*
+ * 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 "FTupleObjects.h"
+
+@implementation FTupleObjects
+
+@synthesize objA;
+@synthesize objB;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.h
new file mode 100644
index 0000000..91ad5e4
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.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 "FTypedefs_Private.h"
+
+@interface FTupleOnDisconnect : NSObject
+
+@property (strong, nonatomic) NSString* pathString;
+@property (strong, nonatomic) NSString* action;
+@property (strong, nonatomic) id data;
+@property (strong, nonatomic) fbt_void_nsstring_nsstring onComplete;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.m
new file mode 100644
index 0000000..bd45822
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleOnDisconnect.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 "FTupleOnDisconnect.h"
+
+@implementation FTupleOnDisconnect
+
+@synthesize pathString;
+@synthesize action;
+@synthesize data;
+@synthesize onComplete;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTuplePathValue.h b/Firebase/Database/Utilities/Tuples/FTuplePathValue.h
new file mode 100644
index 0000000..f7ed423
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTuplePathValue.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>
+
+@class FPath;
+
+@interface FTuplePathValue : NSObject
+@property (nonatomic, strong, readonly) FPath *path;
+@property (nonatomic, strong, readonly) id value;
+- (id) initWithPath:(FPath *)aPath value:(id)aValue;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTuplePathValue.m b/Firebase/Database/Utilities/Tuples/FTuplePathValue.m
new file mode 100644
index 0000000..49240aa
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTuplePathValue.m
@@ -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 "FTuplePathValue.h"
+#import "FPath.h"
+
+@interface FTuplePathValue ()
+@property (nonatomic, strong, readwrite) id value;
+@property (nonatomic, strong, readwrite) FPath *path;
+@end
+
+@implementation FTuplePathValue
+@synthesize path;
+@synthesize value;
+
+- (id) initWithPath:(FPath *)aPath value:(id)aValue {
+ self = [super init];
+ if (self) {
+ self.value = aValue;
+ self.path = aPath;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.h
new file mode 100644
index 0000000..f986916
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.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>
+
+@interface FTupleRemovedQueriesEvents : NSObject
+/**
+* `FIRDatabaseQuery`s removed with [SyncPoint removeEventRegistration:]
+*/
+@property (nonatomic, strong, readonly) NSArray *removedQueries;
+/**
+* cancel events as FEvent
+*/
+@property (nonatomic, strong, readonly) NSArray *cancelEvents;
+
+- (id) initWithRemovedQueries:(NSArray *)removed cancelEvents:(NSArray *)events;
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m
new file mode 100644
index 0000000..818d16b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleRemovedQueriesEvents.m
@@ -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 "FTupleRemovedQueriesEvents.h"
+
+@interface FTupleRemovedQueriesEvents ()
+@property (nonatomic, strong, readwrite) NSArray *removedQueries;
+@property (nonatomic, strong, readwrite) NSArray *cancelEvents;
+@end
+
+@implementation FTupleRemovedQueriesEvents
+@synthesize removedQueries;
+@synthesize cancelEvents;
+
+- (id) initWithRemovedQueries:(NSArray *)removed cancelEvents:(NSArray *)events {
+ self = [super init];
+ if (self) {
+ self.removedQueries = removed;
+ self.cancelEvents = events;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.h
new file mode 100644
index 0000000..5133d6d
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.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 "FPath.h"
+
+@interface FTupleSetIdPath : NSObject
+
+- (id) initWithSetId:(NSNumber *)aSetId andPath:(FPath *)aPath;
+
+@property (strong, nonatomic) NSNumber* setId;
+@property (strong, nonatomic) FPath* path;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m
new file mode 100644
index 0000000..5d3312b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleSetIdPath.m
@@ -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 "FTupleSetIdPath.h"
+
+@implementation FTupleSetIdPath
+
+@synthesize path;
+@synthesize setId;
+
+- (id) initWithSetId:(NSNumber *)aSetId andPath:(FPath *)aPath {
+ self = [super init];
+ if (self) {
+ self.setId = aSetId;
+ self.path = aPath;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleStringNode.h b/Firebase/Database/Utilities/Tuples/FTupleStringNode.h
new file mode 100644
index 0000000..e3fec80
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleStringNode.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 FTupleStringNode : NSObject
+
+- (id)initWithString:(NSString *)aString andNode:(id<FNode>)aNode;
+
+@property (nonatomic, strong) id<FNode> node;
+@property (nonatomic, strong) NSString* string;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleStringNode.m b/Firebase/Database/Utilities/Tuples/FTupleStringNode.m
new file mode 100644
index 0000000..f058a8e
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleStringNode.m
@@ -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 "FTupleStringNode.h"
+
+@implementation FTupleStringNode
+
+@synthesize string;
+@synthesize node;
+
+- (id)initWithString:(NSString *)aString andNode:(id<FNode>)aNode {
+ self = [super init];
+ if (self) {
+ self.string = aString;
+ self.node = aNode;
+ }
+ return self;
+}
+
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTSN.h b/Firebase/Database/Utilities/Tuples/FTupleTSN.h
new file mode 100644
index 0000000..bc62b2d
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTSN.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>
+#import "FTupleStringNode.h"
+
+@interface FTupleTSN : NSObject
+
+@property (nonatomic, strong) FTupleStringNode* from;
+@property (nonatomic, strong) FTupleStringNode* to;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTSN.m b/Firebase/Database/Utilities/Tuples/FTupleTSN.m
new file mode 100644
index 0000000..348c319
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTSN.m
@@ -0,0 +1,24 @@
+/*
+ * 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 "FTupleTSN.h"
+
+@implementation FTupleTSN
+
+@synthesize from;
+@synthesize to;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTransaction.h b/Firebase/Database/Utilities/Tuples/FTupleTransaction.h
new file mode 100644
index 0000000..c9dcf4b
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTransaction.h
@@ -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 <Foundation/Foundation.h>
+#import "FPath.h"
+#import "FTypedefs_Private.h"
+#import "FTypedefs.h"
+
+@interface FTupleTransaction : NSObject
+
+@property (nonatomic, strong) FPath* path;
+@property (nonatomic, copy) fbt_transactionresult_mutabledata update;
+@property (nonatomic, copy) fbt_void_nserror_bool_datasnapshot onComplete;
+@property (nonatomic) FTransactionStatus status;
+
+/**
+* Used when combining transaction at different locations to figure out which one goes first.
+*/
+@property (nonatomic, strong) NSNumber* order;
+/**
+* Whether to raise local events for this transaction
+*/
+@property (nonatomic) BOOL applyLocally;
+
+/**
+* Count how many times we've retried the transaction
+*/
+@property (nonatomic) int retryCount;
+
+/**
+* Function to call to clean up our listener
+*/
+@property (nonatomic, copy) fbt_void_void unwatcher;
+
+/**
+* Stores why a transaction was aborted
+*/
+@property (nonatomic, strong, readonly) NSString* abortStatus;
+@property (nonatomic, strong, readonly) NSString* abortReason;
+
+- (void)setAbortStatus:(NSString *)abortStatus reason:(NSString *)reason;
+- (NSError *)abortError;
+
+@property (nonatomic, strong) NSNumber *currentWriteId;
+
+/**
+* Stores the input snapshot, before the update
+*/
+@property (nonatomic, strong) id<FNode> currentInputSnapshot;
+
+/**
+* Stores the unresolved (for server values) output snapshot, after the update
+*/
+@property (nonatomic, strong) id<FNode> currentOutputSnapshotRaw;
+
+/**
+ * Stores the resolved (for server values) output snapshot, after the update
+ */
+@property (nonatomic, strong) id<FNode> currentOutputSnapshotResolved;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleTransaction.m b/Firebase/Database/Utilities/Tuples/FTupleTransaction.m
new file mode 100644
index 0000000..bcff54e
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleTransaction.m
@@ -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 "FTupleTransaction.h"
+#import "FUtilities.h"
+
+@interface FTupleTransaction ()
+
+@property (nonatomic, strong) NSString *abortStatus;
+@property (nonatomic, strong) NSString *abortReason;
+
+@end
+
+@implementation FTupleTransaction
+
+- (void)setAbortStatus:(NSString *)abortStatus reason:(NSString *)reason {
+ self.abortStatus = abortStatus;
+ self.abortReason = reason;
+}
+
+- (NSError *)abortError {
+ return (self.abortStatus != nil) ? [FUtilities errorForStatus:self.abortStatus andReason:self.abortReason] : nil;
+}
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleUserCallback.h b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.h
new file mode 100644
index 0000000..d598217
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "FTypedefs.h"
+#import "FQueryParams.h"
+
+@interface FTupleUserCallback : NSObject
+
+- (id) initWithHandle:(NSUInteger)handle;
+
+@property (nonatomic, copy) fbt_void_datasnapshot_nsstring datasnapshotPrevnameCallback;
+@property (nonatomic, copy) fbt_void_datasnapshot datasnapshotCallback;
+@property (nonatomic, copy) fbt_void_nserror cancelCallback;
+@property (nonatomic, copy) FQueryParams* queryParams;
+@property (nonatomic) NSUInteger handle;
+
+@end
diff --git a/Firebase/Database/Utilities/Tuples/FTupleUserCallback.m b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.m
new file mode 100644
index 0000000..dc33bbd
--- /dev/null
+++ b/Firebase/Database/Utilities/Tuples/FTupleUserCallback.m
@@ -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 "FTupleUserCallback.h"
+
+@implementation FTupleUserCallback
+
+@synthesize datasnapshotCallback;
+@synthesize datasnapshotPrevnameCallback;
+@synthesize cancelCallback;
+@synthesize queryParams;
+@synthesize handle;
+
+- (id) initWithHandle:(NSUInteger)theHandle {
+ self = [super init];
+ if (self) {
+ self.handle = theHandle;
+ }
+ return self;
+}
+
+@end
diff --git a/Firebase/Database/module.modulemap b/Firebase/Database/module.modulemap
new file mode 100644
index 0000000..28b323e
--- /dev/null
+++ b/Firebase/Database/module.modulemap
@@ -0,0 +1,13 @@
+framework module FirebaseDatabase {
+ umbrella header "FirebaseDatabase.h"
+
+ export *
+ module * { export * }
+
+ link framework "CFNetwork"
+ link framework "Security"
+ link framework "SystemConfiguration"
+
+ link "c++"
+ link "icucore"
+}
diff --git a/Firebase/Database/third_party/SocketRocket/FSRWebSocket.h b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.h
new file mode 100644
index 0000000..dfda376
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.h
@@ -0,0 +1,107 @@
+//
+// Copyright 2012 Square Inc.
+//
+// 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 <Security/SecCertificate.h>
+
+typedef enum {
+ SR_CONNECTING = 0,
+ SR_OPEN = 1,
+ SR_CLOSING = 2,
+ SR_CLOSED = 3,
+
+} FSRReadyState;
+
+@class FSRWebSocket;
+
+extern NSString *const FSRWebSocketErrorDomain;
+
+@protocol FSRWebSocketDelegate;
+
+@interface FSRWebSocket : NSObject <NSStreamDelegate>
+
+@property (nonatomic, weak) id <FSRWebSocketDelegate> delegate;
+
+@property (nonatomic, readonly) FSRReadyState readyState;
+@property (nonatomic, readonly, retain) NSURL *url;
+
+// This returns the negotiated protocol.
+// It will be niluntil after the handshake completes.
+@property (nonatomic, readonly, copy) NSString *protocol;
+
+// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
+- (id)initWithURLRequest:(NSURLRequest *)request queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+- (id)initWithURLRequest:(NSURLRequest *)request;
+
+// Some helper constructors
+- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
+- (id)initWithURL:(NSURL *)url;
+
+// Delegate queue will be dispatch_main_queue by default.
+// You cannot set both OperationQueue and dispatch_queue.
+- (void)setDelegateOperationQueue:(NSOperationQueue*) queue;
+- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue;
+
+// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes.
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+
+// SRWebSockets are intended one-time-use only. Open should be called once and only once
+- (void)open;
+
+- (void)close;
+- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
+
+// Send a UTF8 String or Data
+- (void)send:(id)data;
+
+@end
+
+@protocol FSRWebSocketDelegate <NSObject>
+
+// message will either be an NSString if the server is using text
+// or NSData if the server is using binary
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message;
+
+@optional
+
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket;
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error;
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
+
+@end
+
+
+@interface NSURLRequest (FCertificateAdditions)
+
+@property (nonatomic, retain, readonly) NSArray *FSR_SSLPinnedCertificates;
+
+@end
+
+
+@interface NSMutableURLRequest (FCertificateAdditions)
+
+@property (nonatomic, retain) NSArray *FSR_SSLPinnedCertificates;
+
+@end
+
+@interface NSRunLoop (FSRWebSocket)
+
++ (NSRunLoop *)FSR_networkRunLoop;
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m
new file mode 100644
index 0000000..c2b395c
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m
@@ -0,0 +1,1848 @@
+//
+// Copyright 2012 Square Inc.
+//
+// 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 "FSRWebSocket.h"
+
+#if TARGET_OS_IPHONE
+#define HAS_ICU
+#endif
+
+#import <sys/socket.h>
+
+#ifdef HAS_ICU
+#import <unicode/utf8.h>
+#endif
+
+#if TARGET_OS_IPHONE
+#import <Endian.h>
+#else
+#import <CoreServices/CoreServices.h>
+#endif
+
+#import <CommonCrypto/CommonDigest.h>
+#import <Security/SecRandom.h>
+#import "fbase64.h"
+#import "NSData+SRB64Additions.h"
+
+#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE
+#define sr_dispatch_retain(x)
+#define sr_dispatch_release(x)
+#define maybe_bridge(x) ((__bridge void *) x)
+#else
+#define sr_dispatch_retain(x) dispatch_retain(x)
+#define sr_dispatch_release(x) dispatch_release(x)
+#define maybe_bridge(x) (x)
+#endif
+
+typedef enum {
+ SROpCodeTextFrame = 0x1,
+ SROpCodeBinaryFrame = 0x2,
+ //3-7Reserved
+ SROpCodeConnectionClose = 0x8,
+ SROpCodePing = 0x9,
+ SROpCodePong = 0xA,
+ //B-F reserved
+} FSROpCode;
+
+typedef enum {
+ SRStatusCodeNormal = 1000,
+ SRStatusCodeGoingAway = 1001,
+ SRStatusCodeProtocolError = 1002,
+ SRStatusCodeUnhandledType = 1003,
+ // 1004 reserved
+ SRStatusNoStatusReceived = 1005,
+ // 1004-1006 reserved
+ SRStatusCodeInvalidUTF8 = 1007,
+ SRStatusCodePolicyViolated = 1008,
+ SRStatusCodeMessageTooBig = 1009,
+} FSRStatusCode;
+
+typedef struct {
+ BOOL fin;
+// BOOL rsv1;
+// BOOL rsv2;
+// BOOL rsv3;
+ uint8_t opcode;
+ BOOL masked;
+ uint64_t payload_length;
+} frame_header;
+
+static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+static inline int32_t validate_dispatch_data_partial_string(NSData *data);
+static inline void SRFastLog(NSString *format, ...);
+
+@interface NSData (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+
+@end
+
+
+@interface NSString (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+
+@end
+
+
+@interface NSURL (FSRWebSocket)
+
+// The origin isn't really applicable for a native application
+// So instead, just map ws -> http and wss -> https
+- (NSString *)SR_origin;
+
+@end
+
+@interface _FSRRunLoopThread : NSThread
+
+@property (nonatomic, readonly) NSRunLoop *runLoop;
+
+@end
+
+static NSString *newSHA1String(const char *bytes, size_t length) {
+ uint8_t md[CC_SHA1_DIGEST_LENGTH];
+
+ CC_SHA1(bytes, (int)length, md);
+
+ size_t buffer_size = ((sizeof(md) * 3 + 2) / 2);
+
+ char *buffer = (char *)malloc(buffer_size);
+
+ int len = f_b64_ntop(md, CC_SHA1_DIGEST_LENGTH, buffer, buffer_size);
+ if (len == -1) {
+ free(buffer);
+ return nil;
+ } else{
+ return [[NSString alloc] initWithBytesNoCopy:buffer length:len encoding:NSASCIIStringEncoding freeWhenDone:YES];
+ }
+}
+
+@implementation NSData (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+{
+ return newSHA1String(self.bytes, self.length);
+}
+
+@end
+
+
+@implementation NSString (FSRWebSocket)
+
+- (NSString *)stringBySHA1ThenBase64Encoding;
+{
+ return newSHA1String(self.UTF8String, self.length);
+}
+
+@end
+
+NSString *const FSRWebSocketErrorDomain = @"FSRWebSocketErrorDomain";
+
+// Returns number of bytes consumed. returning 0 means you didn't match.
+// Sends bytes to callback handler;
+typedef size_t (^stream_scanner)(NSData *collected_data);
+
+typedef void (^data_callback)(FSRWebSocket *webSocket, NSData *data);
+
+@interface FSRIOConsumer : NSObject {
+ stream_scanner _scanner;
+ data_callback _handler;
+ size_t _bytesNeeded;
+ BOOL _readToCurrentFrame;
+ BOOL _unmaskBytes;
+}
+@property (nonatomic, copy, readonly) stream_scanner consumer;
+@property (nonatomic, copy, readonly) data_callback handler;
+@property (nonatomic, assign) size_t bytesNeeded;
+@property (nonatomic, assign, readonly) BOOL readToCurrentFrame;
+@property (nonatomic, assign, readonly) BOOL unmaskBytes;
+
+@end
+
+// This class is not thread-safe, and is expected to always be run on the same queue.
+@interface FSRIOConsumerPool : NSObject
+
+- (id)initWithBufferCapacity:(NSUInteger)poolSize;
+
+- (FSRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+- (void)returnConsumer:(FSRIOConsumer *)consumer;
+
+@end
+
+@interface FSRWebSocket () <NSStreamDelegate>
+
+- (void)_writeData:(NSData *)data;
+- (void)_closeWithProtocolError:(NSString *)message;
+- (void)_failWithError:(NSError *)error;
+
+- (void)_disconnect;
+
+- (void)_readFrameNew;
+- (void)_readFrameContinue;
+
+- (void)_pumpScanner;
+
+- (void)_pumpWriting;
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback;
+- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;
+- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;
+- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;
+
+- (void)_sendFrameWithOpcode:(FSROpCode)opcode data:(id)data;
+
+- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;
+- (void)_SR_commonInit;
+
+- (void)_initializeStreams;
+- (void)_connect;
+
+@property (nonatomic) FSRReadyState readyState;
+
+@property (nonatomic) NSOperationQueue *delegateOperationQueue;
+@property (nonatomic) dispatch_queue_t delegateDispatchQueue;
+
+@end
+
+
+@implementation FSRWebSocket {
+ NSInteger _webSocketVersion;
+
+ NSOperationQueue *_delegateOperationQueue;
+ dispatch_queue_t _delegateDispatchQueue;
+ dispatch_queue_t _workQueue;
+ NSMutableArray *_consumers;
+
+ NSInputStream *_inputStream;
+ NSOutputStream *_outputStream;
+
+ NSMutableData *_readBuffer;
+ NSInteger _readBufferOffset;
+
+ NSMutableData *_outputBuffer;
+ NSInteger _outputBufferOffset;
+
+ uint8_t _currentFrameOpcode;
+ size_t _currentFrameCount;
+ size_t _readOpCount;
+ uint32_t _currentStringScanPosition;
+ NSMutableData *_currentFrameData;
+
+ NSString *_closeReason;
+
+ NSString *_secKey;
+
+ BOOL _pinnedCertFound;
+
+ uint8_t _currentReadMaskKey[4];
+ size_t _currentReadMaskOffset;
+
+ BOOL _consumerStopped;
+
+ BOOL _closeWhenFinishedWriting;
+ BOOL _failed;
+
+ BOOL _secure;
+ NSURLRequest *_urlRequest;
+ NSString *_userAgent;
+
+ CFHTTPMessageRef _receivedHTTPHeaders;
+
+ BOOL _sentClose;
+ BOOL _didFail;
+ BOOL _cleanupScheduled;
+ int _closeCode;
+
+ BOOL _isPumping;
+
+ NSMutableSet *_scheduledRunloops;
+
+ // We use this to retain ourselves.
+ __strong FSRWebSocket *_selfRetain;
+
+ NSArray *_requestedProtocols;
+ FSRIOConsumerPool *_consumerPool;
+}
+
+@synthesize delegate = _delegate;
+@synthesize url = _url;
+@synthesize readyState = _readyState;
+@synthesize protocol = _protocol;
+
+static __strong NSData *CRLFCRLF;
+
++ (void)initialize;
+{
+ CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+{
+ self = [super init];
+ if (self) {
+ assert(request.URL);
+ _url = request.URL;
+ NSString *scheme = [_url scheme];
+
+ _requestedProtocols = [protocols copy];
+ _userAgent = userAgent;
+
+ assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]);
+ _urlRequest = request;
+
+ if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) {
+ _secure = YES;
+ }
+
+ if (!queue) {
+ _delegateDispatchQueue = dispatch_get_main_queue();
+ } else {
+ _delegateDispatchQueue = queue;
+ }
+
+ [self _SR_commonInit];
+ }
+
+ return self;
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
+{
+ return [self initWithURLRequest:request protocols:nil queue:nil andUserAgent:nil];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request queue:(dispatch_queue_t)queue andUserAgent:(NSString *)userAgent;
+{
+ return [self initWithURLRequest:request protocols:nil queue:queue andUserAgent:userAgent];
+}
+
+- (id)initWithURLRequest:(NSURLRequest *)request;
+{
+ return [self initWithURLRequest:request protocols:nil];
+}
+
+- (id)initWithURL:(NSURL *)url;
+{
+ return [self initWithURL:url protocols:nil];
+}
+
+- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
+{
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
+ return [self initWithURLRequest:request protocols:protocols];
+}
+
+- (void)_SR_commonInit;
+{
+ _readyState = SR_CONNECTING;
+
+ _consumerStopped = YES;
+
+ _webSocketVersion = 13;
+
+ _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
+
+ // Going to set a specific on the queue so we can validate we're on the work queue
+ dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL);
+
+ sr_dispatch_retain(_delegateDispatchQueue);
+
+ _readBuffer = [[NSMutableData alloc] init];
+ _outputBuffer = [[NSMutableData alloc] init];
+
+ _currentFrameData = [[NSMutableData alloc] init];
+
+ _consumers = [[NSMutableArray alloc] init];
+
+ _consumerPool = [[FSRIOConsumerPool alloc] init];
+
+ _scheduledRunloops = [[NSMutableSet alloc] init];
+
+ [self _initializeStreams];
+
+ // default handlers
+}
+
+- (void)assertOnWorkQueue;
+{
+ assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue));
+}
+
+- (void)dealloc
+{
+ _inputStream.delegate = nil;
+ _outputStream.delegate = nil;
+
+ [_inputStream close];
+ [_outputStream close];
+
+ sr_dispatch_release(_workQueue);
+ _workQueue = NULL;
+
+ if (_receivedHTTPHeaders) {
+ CFRelease(_receivedHTTPHeaders);
+ _receivedHTTPHeaders = NULL;
+ }
+
+ if (_delegateDispatchQueue) {
+ sr_dispatch_release(_delegateDispatchQueue);
+ _delegateDispatchQueue = NULL;
+ }
+}
+
+#ifndef NDEBUG
+
+- (void)setReadyState:(FSRReadyState)aReadyState;
+{
+ [self willChangeValueForKey:@"readyState"];
+ assert(aReadyState > _readyState);
+ _readyState = aReadyState;
+ [self didChangeValueForKey:@"readyState"];
+}
+
+#endif
+
+- (void)open;
+{
+ assert(_url);
+ NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once");
+
+ _selfRetain = self;
+
+ [self _connect];
+}
+
+// Calls block on delegate queue
+- (void)_performDelegateBlock:(dispatch_block_t)block;
+{
+ if (_delegateOperationQueue) {
+ [_delegateOperationQueue addOperationWithBlock:block];
+ } else {
+ assert(_delegateDispatchQueue);
+ dispatch_async(_delegateDispatchQueue, block);
+ }
+}
+
+- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue;
+{
+ if (queue) {
+ sr_dispatch_retain(queue);
+ }
+
+ if (_delegateDispatchQueue) {
+ sr_dispatch_release(_delegateDispatchQueue);
+ }
+
+ _delegateDispatchQueue = queue;
+}
+
+- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage;
+{
+ NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept")));
+
+ if (acceptHeader == nil) {
+ return NO;
+ }
+
+ NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString];
+ NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding];
+
+ return [acceptHeader isEqualToString:expectedAccept];
+}
+
+- (void)_HTTPHeadersDidFinish;
+{
+ NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders);
+
+ if (responseCode >= 400) {
+ SRFastLog(@"Request failed with response code %d", responseCode);
+ [self _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:2132 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"received bad response code from server %u", (int)responseCode] forKey:NSLocalizedDescriptionKey]]];
+ return;
+
+ }
+
+ if(![self _checkHandshake:_receivedHTTPHeaders]) {
+ [self _failWithError:[NSError errorWithDomain:FSRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+
+ NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol")));
+ if (negotiatedProtocol) {
+ // Make sure we requested the protocol
+ if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) {
+ [self _failWithError:[NSError errorWithDomain:FSRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+
+ _protocol = negotiatedProtocol;
+ }
+
+ self.readyState = SR_OPEN;
+
+ if (!_didFail) {
+ [self _readFrameNew];
+ }
+
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) {
+ [self.delegate webSocketDidOpen:self];
+ };
+ }];
+}
+
+
+- (void)_readHTTPHeader;
+{
+ if (_receivedHTTPHeaders == NULL) {
+ _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO);
+ }
+
+ [self _readUntilHeaderCompleteWithCallback:^(FSRWebSocket *self, NSData *data) {
+ CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length);
+
+ if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) {
+ SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders)));
+ [self _HTTPHeadersDidFinish];
+ } else {
+ [self _readHTTPHeader];
+ }
+ }];
+}
+
+- (void)didConnect
+{
+ SRFastLog(@"Connected");
+ CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1);
+
+ // Set host first so it defaults
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host));
+
+ NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16];
+ int result = SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes);
+ assert(result == 0);
+ _secKey = [FSRUtilities base64EncodedStringFromData:keyBytes];
+ assert([_secKey length] == 24);
+
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket"));
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade"));
+ if (_userAgent) {
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("User-Agent"), (__bridge CFStringRef)_userAgent);
+ }
+
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey);
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%u", (int)_webSocketVersion]);
+
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin);
+
+ if (_requestedProtocols) {
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]);
+ }
+
+ [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
+ }];
+
+ NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request));
+
+ CFRelease(request);
+
+ [self _writeData:message];
+ [self _readHTTPHeader];
+}
+
+//- (void)_connectToHost:(NSString *)host port:(NSInteger)port;
+- (void)_initializeStreams;
+{
+ NSInteger port = _url.port.integerValue;
+ if (port == 0) {
+ if (!_secure) {
+ port = 80;
+ } else {
+ port = 443;
+ }
+ }
+ NSString *host = _url.host;
+
+ CFReadStreamRef readStream = NULL;
+ CFWriteStreamRef writeStream = NULL;
+
+ CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, (int)port, &readStream, &writeStream);
+
+ // XXX
+ CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeBackground);
+ CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeBackground);
+
+ _outputStream = CFBridgingRelease(writeStream);
+ _inputStream = CFBridgingRelease(readStream);
+
+
+ if (_secure) {
+ NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init];
+
+ [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel];
+
+ // If we're using pinned certs, don't validate the certificate chain
+ if ([_urlRequest FSR_SSLPinnedCertificates].count) {
+ [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain];
+ }
+
+ [_outputStream setProperty:SSLOptions
+ forKey:(__bridge id)kCFStreamPropertySSLSettings];
+ }
+
+ _inputStream.delegate = self;
+ _outputStream.delegate = self;
+
+ [_outputStream open];
+ [_inputStream open];
+}
+
+- (void)_connect;
+{
+ if (!_scheduledRunloops.count) {
+ [self scheduleInRunLoop:[NSRunLoop FSR_networkRunLoop] forMode:NSDefaultRunLoopMode];
+ }
+
+
+ [_outputStream open];
+ [_inputStream open];
+}
+
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+{
+ [_outputStream scheduleInRunLoop:aRunLoop forMode:mode];
+ [_inputStream scheduleInRunLoop:aRunLoop forMode:mode];
+
+ [_scheduledRunloops addObject:@[aRunLoop, mode]];
+}
+
+- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+{
+ [_outputStream removeFromRunLoop:aRunLoop forMode:mode];
+ [_inputStream removeFromRunLoop:aRunLoop forMode:mode];
+
+ [_scheduledRunloops removeObject:@[aRunLoop, mode]];
+}
+
+- (void)close;
+{
+ [self closeWithCode:-1 reason:nil];
+}
+
+- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
+{
+ assert(code);
+ dispatch_async(_workQueue, ^{
+ if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) {
+ return;
+ }
+
+ BOOL wasConnecting = self.readyState == SR_CONNECTING;
+
+ self.readyState = SR_CLOSING;
+
+ SRFastLog(@"Closing with code %d reason %@", code, reason);
+
+ if (wasConnecting) {
+ [self _disconnect];
+ return;
+ }
+
+ size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+ NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize];
+ NSData *payload = mutablePayload;
+
+ ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(code);
+
+ if (reason) {
+ NSRange remainingRange = {0};
+
+ NSUInteger usedLength = 0;
+
+ BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange];
+
+ assert(success);
+ assert(remainingRange.length == 0);
+
+ if (usedLength != maxMsgSize) {
+ payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))];
+ }
+ }
+
+
+ [self _sendFrameWithOpcode:SROpCodeConnectionClose data:payload];
+ });
+}
+
+- (void)_closeWithProtocolError:(NSString *)message;
+{
+ // Need to shunt this on the _callbackQueue first to see if they received any messages
+ [self _performDelegateBlock:^{
+ [self closeWithCode:SRStatusCodeProtocolError reason:message];
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+ }];
+}
+
+- (void)_failWithError:(NSError *)error;
+{
+ dispatch_async(_workQueue, ^{
+ if (self.readyState != SR_CLOSED) {
+ _failed = YES;
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) {
+ [self.delegate webSocket:self didFailWithError:error];
+ }
+ }];
+
+ self.readyState = SR_CLOSED;
+
+ SRFastLog(@"Failing with error %@", error.localizedDescription);
+
+ [self _disconnect];
+ [self _scheduleCleanup];
+ }
+ });
+}
+
+- (void)_writeData:(NSData *)data;
+{
+ [self assertOnWorkQueue];
+
+ if (_closeWhenFinishedWriting) {
+ return;
+ }
+ [_outputBuffer appendData:data];
+ [self _pumpWriting];
+}
+- (void)send:(id)data;
+{
+ SRFastLog(@"Sending data %@", data);
+ NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open");
+ // TODO: maybe not copy this for performance
+ data = [data copy];
+ dispatch_async(_workQueue, ^{
+ if ([data isKindOfClass:[NSString class]]) {
+ [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]];
+ } else if ([data isKindOfClass:[NSData class]]) {
+ [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data];
+ } else if (data == nil) {
+ [self _sendFrameWithOpcode:SROpCodeTextFrame data:data];
+ } else {
+ assert(NO);
+ }
+ });
+}
+
+- (void)handlePing:(NSData *)pingData;
+{
+ // Need to pingpong this off _callbackQueue first to make sure messages happen in order
+ [self _performDelegateBlock:^{
+ dispatch_async(_workQueue, ^{
+ [self _sendFrameWithOpcode:SROpCodePong data:pingData];
+ });
+ }];
+}
+
+- (void)handlePong;
+{
+ // NOOP
+}
+
+- (void)_handleMessage:(id)message
+{
+ SRFastLog(@"Received message");
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didReceiveMessage:)]) {
+ [self.delegate webSocket:self didReceiveMessage:message];
+ }
+ }];
+}
+
+
+static inline BOOL closeCodeIsValid(int closeCode) {
+ if (closeCode < 1000) {
+ return NO;
+ }
+
+ if (closeCode >= 1000 && closeCode <= 1011) {
+ if (closeCode == 1004 ||
+ closeCode == 1005 ||
+ closeCode == 1006) {
+ return NO;
+ }
+ return YES;
+ }
+
+ if (closeCode >= 3000 && closeCode <= 3999) {
+ return YES;
+ }
+
+ if (closeCode >= 4000 && closeCode <= 4999) {
+ return YES;
+ }
+
+ return NO;
+}
+
+// Note from RFC:
+//
+// If there is a body, the first two
+// bytes of the body MUST be a 2-byte unsigned integer (in network byte
+// order) representing a status code with value /code/ defined in
+// Section 7.4. Following the 2-byte integer the body MAY contain UTF-8
+// encoded data with value /reason/, the interpretation of which is not
+// defined by this specification.
+
+- (void)handleCloseWithData:(NSData *)data;
+{
+ size_t dataSize = data.length;
+ __block uint16_t closeCode = 0;
+
+ SRFastLog(@"Received close frame");
+
+ if (dataSize == 1) {
+ // TODO handle error
+ [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"];
+ return;
+ } else if (dataSize >= 2) {
+ [data getBytes:&closeCode length:sizeof(closeCode)];
+ _closeCode = EndianU16_BtoN(closeCode);
+ if (!closeCodeIsValid(_closeCode)) {
+ [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]];
+ return;
+ }
+ if (dataSize > 2) {
+ _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding];
+ if (!_closeReason) {
+ [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"];
+ return;
+ }
+ }
+ } else {
+ _closeCode = SRStatusNoStatusReceived;
+ }
+
+ [self assertOnWorkQueue];
+
+ if (self.readyState == SR_OPEN) {
+ [self closeWithCode:1000 reason:nil];
+ }
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+}
+
+- (void)_disconnect;
+{
+ [self assertOnWorkQueue];
+ SRFastLog(@"Trying to disconnect");
+ _closeWhenFinishedWriting = YES;
+ [self _pumpWriting];
+}
+
+- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode;
+{
+ // Check that the current data is valid UTF8
+
+ BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose);
+ if (!isControlFrame) {
+ [self _readFrameNew];
+ } else {
+ dispatch_async(_workQueue, ^{
+ [self _readFrameContinue];
+ });
+ }
+
+ switch (opcode) {
+ case SROpCodeTextFrame: {
+ NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding];
+ if (str == nil && frameData) {
+ [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"];
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+
+ return;
+ }
+ [self _handleMessage:str];
+ break;
+ }
+ case SROpCodeBinaryFrame:
+ [self _handleMessage:[frameData copy]];
+ break;
+ case SROpCodeConnectionClose:
+ [self handleCloseWithData:frameData];
+ break;
+ case SROpCodePing:
+ [self handlePing:frameData];
+ break;
+ case SROpCodePong:
+ [self handlePong];
+ break;
+ default:
+ [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %u", (int)opcode]];
+ // TODO: Handle invalid opcode
+ break;
+ }
+}
+
+- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData;
+{
+ assert(frame_header.opcode != 0);
+
+ if (self.readyState != SR_OPEN) {
+ return;
+ }
+
+
+ BOOL isControlFrame = (frame_header.opcode == SROpCodePing || frame_header.opcode == SROpCodePong || frame_header.opcode == SROpCodeConnectionClose);
+
+ if (isControlFrame && !frame_header.fin) {
+ [self _closeWithProtocolError:@"Fragmented control frames not allowed"];
+ return;
+ }
+
+ if (isControlFrame && frame_header.payload_length >= 126) {
+ [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"];
+ return;
+ }
+
+ if (!isControlFrame) {
+ _currentFrameOpcode = frame_header.opcode;
+ _currentFrameCount += 1;
+ }
+
+ if (frame_header.payload_length == 0) {
+ if (isControlFrame) {
+ [self _handleFrameWithData:curData opCode:frame_header.opcode];
+ } else {
+ if (frame_header.fin) {
+ [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode];
+ } else {
+ // TODO add assert that opcode is not a control;
+ [self _readFrameContinue];
+ }
+ }
+ } else {
+ [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(FSRWebSocket *self, NSData *newData) {
+ if (isControlFrame) {
+ [self _handleFrameWithData:newData opCode:frame_header.opcode];
+ } else {
+ if (frame_header.fin) {
+ [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode];
+ } else {
+ // TODO add assert that opcode is not a control;
+ [self _readFrameContinue];
+ }
+
+ }
+ } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked];
+ }
+}
+
+/* From RFC:
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-------+-+-------------+-------------------------------+
+ |F|R|R|R| opcode|M| Payload len | Extended payload length |
+ |I|S|S|S| (4) |A| (7) | (16/64) |
+ |N|V|V|V| |S| | (if payload len==126/127) |
+ | |1|2|3| |K| | |
+ +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ | Extended payload length continued, if payload len == 127 |
+ + - - - - - - - - - - - - - - - +-------------------------------+
+ | |Masking-key, if MASK set to 1 |
+ +-------------------------------+-------------------------------+
+ | Masking-key (continued) | Payload Data |
+ +-------------------------------- - - - - - - - - - - - - - - - +
+ : Payload Data continued ... :
+ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ | Payload Data continued ... |
+ +---------------------------------------------------------------+
+ */
+
+static const uint8_t SRFinMask = 0x80;
+static const uint8_t SROpCodeMask = 0x0F;
+static const uint8_t SRRsvMask = 0x70;
+static const uint8_t SRMaskMask = 0x80;
+static const uint8_t SRPayloadLenMask = 0x7F;
+
+
+- (void)_readFrameContinue;
+{
+ assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0));
+
+ [self _addConsumerWithDataLength:2 callback:^(FSRWebSocket *self, NSData *data) {
+ __block frame_header header = {0};
+
+ const uint8_t *headerBuffer = data.bytes;
+ assert(data.length >= 2);
+
+ if (headerBuffer[0] & SRRsvMask) {
+ [self _closeWithProtocolError:@"Server used RSV bits"];
+ return;
+ }
+
+ uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]);
+
+ BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose);
+
+ if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) {
+ [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"];
+ return;
+ }
+
+ if (receivedOpcode == 0 && self->_currentFrameCount == 0) {
+ [self _closeWithProtocolError:@"cannot continue a message"];
+ return;
+ }
+
+ header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode;
+
+ header.fin = !!(SRFinMask & headerBuffer[0]);
+
+
+ header.masked = !!(SRMaskMask & headerBuffer[1]);
+ header.payload_length = SRPayloadLenMask & headerBuffer[1];
+
+ headerBuffer = NULL;
+
+ if (header.masked) {
+ [self _closeWithProtocolError:@"Client must receive unmasked data"];
+ }
+
+ size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0;
+
+ if (header.payload_length == 126) {
+ extra_bytes_needed += sizeof(uint16_t);
+ } else if (header.payload_length == 127) {
+ extra_bytes_needed += sizeof(uint64_t);
+ }
+
+ if (extra_bytes_needed == 0) {
+ [self _handleFrameHeader:header curData:self->_currentFrameData];
+ } else {
+ [self _addConsumerWithDataLength:extra_bytes_needed callback:^(FSRWebSocket *self, NSData *data) {
+ size_t mapped_size = data.length;
+ const void *mapped_buffer = data.bytes;
+ size_t offset = 0;
+
+ if (header.payload_length == 126) {
+ assert(mapped_size >= sizeof(uint16_t));
+ uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer));
+ header.payload_length = newLen;
+ offset += sizeof(uint16_t);
+ } else if (header.payload_length == 127) {
+ assert(mapped_size >= sizeof(uint64_t));
+ header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer));
+ offset += sizeof(uint64_t);
+ } else {
+ assert(header.payload_length < 126 && header.payload_length >= 0);
+ }
+
+
+ if (header.masked) {
+ assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset);
+ memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey));
+ }
+
+ [self _handleFrameHeader:header curData:self->_currentFrameData];
+ } readToCurrentFrame:NO unmaskBytes:NO];
+ }
+ } readToCurrentFrame:NO unmaskBytes:NO];
+}
+
+- (void)_readFrameNew;
+{
+ dispatch_async(_workQueue, ^{
+ [_currentFrameData setLength:0];
+
+ _currentFrameOpcode = 0;
+ _currentFrameCount = 0;
+ _readOpCount = 0;
+ _currentStringScanPosition = 0;
+
+ [self _readFrameContinue];
+ });
+}
+
+- (void)_pumpWriting;
+{
+ [self assertOnWorkQueue];
+
+ NSUInteger dataLength = _outputBuffer.length;
+ if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) {
+ NSUInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset];
+ if (bytesWritten == -1) {
+ [self _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+
+ _outputBufferOffset += bytesWritten;
+
+ if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) {
+ _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset];
+ _outputBufferOffset = 0;
+ }
+ }
+
+ if (_closeWhenFinishedWriting &&
+ _outputBuffer.length - _outputBufferOffset == 0 &&
+ (_inputStream.streamStatus != NSStreamStatusNotOpen &&
+ _inputStream.streamStatus != NSStreamStatusClosed) &&
+ !_sentClose) {
+ _sentClose = YES;
+
+ @synchronized (self) {
+ [_outputStream close];
+ [_inputStream close];
+
+ // TODO: Why are we missing the SocketRocket code to call unscheduleFromRunLoop???
+ }
+
+ if (!_failed) {
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
+ [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES];
+ }
+ }];
+ }
+ [self _scheduleCleanup];
+ }
+}
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback;
+{
+ [self assertOnWorkQueue];
+ [self _addConsumerWithScanner:consumer callback:callback dataLength:0];
+}
+
+- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+ [self assertOnWorkQueue];
+ assert(dataLength);
+
+ [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]];
+ [self _pumpScanner];
+}
+
+- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength;
+{
+ [self assertOnWorkQueue];
+ [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]];
+ [self _pumpScanner];
+}
+
+
+- (void)_scheduleCleanup
+{
+ @synchronized(self) {
+ if (_cleanupScheduled) {
+ return;
+ }
+
+ _cleanupScheduled = YES;
+
+ // Cleanup NSStream delegate's in the same RunLoop used by the streams themselves:
+ // This way we'll prevent race conditions between handleEvent and SRWebsocket's dealloc
+ NSTimer *timer = [NSTimer timerWithTimeInterval:(0.0f) target:self selector:@selector(_cleanupSelfReference:) userInfo:nil repeats:NO];
+ [[NSRunLoop FSR_networkRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
+ }
+}
+
+- (void)_cleanupSelfReference:(NSTimer *)timer
+{
+ @synchronized(self) {
+ // Nuke NSStream delegate's
+ _inputStream.delegate = nil;
+ _outputStream.delegate = nil;
+
+ // Remove the streams, right now, from the networkRunLoop
+ [_inputStream close];
+ [_outputStream close];
+ }
+
+ // Cleanup selfRetain in the same GCD queue as usual
+ dispatch_async(_workQueue, ^{
+ _selfRetain = nil;
+ });
+}
+
+
+static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'};
+
+- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler;
+{
+ [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler];
+}
+
+- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler;
+{
+ // TODO optimize so this can continue from where we last searched
+ stream_scanner consumer = ^size_t(NSData *data) {
+ __block size_t found_size = 0;
+ __block size_t match_count = 0;
+
+ size_t size = data.length;
+ const unsigned char *buffer = data.bytes;
+ for (int i = 0; i < size; i++ ) {
+ if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) {
+ match_count += 1;
+ if (match_count == length) {
+ found_size = i + 1;
+ break;
+ }
+ } else {
+ match_count = 0;
+ }
+ }
+ return found_size;
+ };
+ [self _addConsumerWithScanner:consumer callback:dataHandler];
+}
+
+
+// Returns true if did work
+- (BOOL)_innerPumpScanner {
+
+ BOOL didWork = NO;
+
+ if (self.readyState >= SR_CLOSING) {
+ return didWork;
+ }
+
+ if (!_consumers.count) {
+ return didWork;
+ }
+
+ size_t curSize = _readBuffer.length - _readBufferOffset;
+ if (!curSize) {
+ return didWork;
+ }
+
+ FSRIOConsumer *consumer = [_consumers objectAtIndex:0];
+
+ size_t bytesNeeded = consumer.bytesNeeded;
+
+ size_t foundSize = 0;
+ if (consumer.consumer) {
+ NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO];
+ foundSize = consumer.consumer(tempView);
+ } else {
+ assert(consumer.bytesNeeded);
+ if (curSize >= bytesNeeded) {
+ foundSize = bytesNeeded;
+ } else if (consumer.readToCurrentFrame) {
+ foundSize = curSize;
+ }
+ }
+
+ NSData *slice = nil;
+ if (consumer.readToCurrentFrame || foundSize) {
+ NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize);
+ slice = [_readBuffer subdataWithRange:sliceRange];
+
+ _readBufferOffset += foundSize;
+
+ if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) {
+ _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0;
+ }
+
+ if (consumer.unmaskBytes) {
+ NSMutableData *mutableSlice = [slice mutableCopy];
+
+ NSUInteger len = mutableSlice.length;
+ uint8_t *bytes = mutableSlice.mutableBytes;
+
+ for (int i = 0; i < len; i++) {
+ bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)];
+ _currentReadMaskOffset += 1;
+ }
+
+ slice = mutableSlice;
+ }
+
+ if (consumer.readToCurrentFrame) {
+ [_currentFrameData appendData:slice];
+
+ _readOpCount += 1;
+
+ if (_currentFrameOpcode == SROpCodeTextFrame) {
+ // Validate UTF8 stuff.
+ size_t currentDataSize = _currentFrameData.length;
+ if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) {
+ // TODO: Optimize the crap out of this. Don't really have to copy all the data each time
+
+ size_t scanSize = currentDataSize - _currentStringScanPosition;
+
+ NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)];
+ int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data);
+
+ if (valid_utf8_size == -1) {
+ [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"];
+ dispatch_async(_workQueue, ^{
+ [self _disconnect];
+ });
+ return didWork;
+ } else {
+ _currentStringScanPosition += valid_utf8_size;
+ }
+ }
+
+ }
+
+ consumer.bytesNeeded -= foundSize;
+
+ if (consumer.bytesNeeded == 0) {
+ [_consumers removeObjectAtIndex:0];
+ consumer.handler(self, nil);
+ didWork = YES;
+ }
+ } else if (foundSize) {
+ [_consumers removeObjectAtIndex:0];
+ consumer.handler(self, slice);
+ didWork = YES;
+ }
+ }
+ return didWork;
+}
+
+-(void)_pumpScanner;
+{
+ [self assertOnWorkQueue];
+
+ if (!_isPumping) {
+ _isPumping = YES;
+ } else {
+ return;
+ }
+
+ while ([self _innerPumpScanner]) {
+
+ }
+
+ _isPumping = NO;
+}
+
+//#define NOMASK
+
+static const size_t SRFrameHeaderOverhead = 32;
+
+- (void)_sendFrameWithOpcode:(FSROpCode)opcode data:(id)data;
+{
+ [self assertOnWorkQueue];
+
+ NSAssert(data == nil || [data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"Function expects nil, NSString or NSData");
+
+ size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length];
+
+ NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead];
+ if (!frame) {
+ [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"];
+ return;
+ }
+ uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes];
+
+ // set fin
+ frame_buffer[0] = SRFinMask | opcode;
+
+ BOOL useMask = YES;
+#ifdef NOMASK
+ useMask = NO;
+#endif
+
+ if (useMask) {
+ // set the mask and header
+ frame_buffer[1] |= SRMaskMask;
+ }
+
+ size_t frame_buffer_size = 2;
+
+ const uint8_t *unmasked_payload = NULL;
+ if ([data isKindOfClass:[NSData class]]) {
+ unmasked_payload = (uint8_t *)[data bytes];
+ } else if ([data isKindOfClass:[NSString class]]) {
+ unmasked_payload = (const uint8_t *)[data UTF8String];
+ } else {
+ assert(NO);
+ }
+
+ if (payloadLength < 126) {
+ frame_buffer[1] |= payloadLength;
+ } else if (payloadLength <= UINT16_MAX) {
+ frame_buffer[1] |= 126;
+ *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength);
+ frame_buffer_size += sizeof(uint16_t);
+ } else {
+ frame_buffer[1] |= 127;
+ *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength);
+ frame_buffer_size += sizeof(uint64_t);
+ }
+
+ if (!useMask) {
+ for (int i = 0; i < payloadLength; i++) {
+ frame_buffer[frame_buffer_size] = unmasked_payload[i];
+ frame_buffer_size += 1;
+ }
+ } else {
+ uint8_t *mask_key = frame_buffer + frame_buffer_size;
+ int result = SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key);
+ assert(result == 0);
+ frame_buffer_size += sizeof(uint32_t);
+
+ // TODO: could probably optimize this with SIMD
+ for (int i = 0; i < payloadLength; i++) {
+ frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)];
+ frame_buffer_size += 1;
+ }
+ }
+
+ assert(frame_buffer_size <= [frame length]);
+ frame.length = frame_buffer_size;
+
+ [self _writeData:frame];
+}
+
+- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;
+{
+ __weak __typeof__(self) weakSelf = self;
+
+ // turn on keep-alive for the output stream.
+ if (eventCode == NSStreamEventOpenCompleted && aStream == _outputStream) {
+ CFDataRef socketData = CFWriteStreamCopyProperty((CFWriteStreamRef)_outputStream, kCFStreamPropertySocketNativeHandle);
+ // In rare cases socketData might be nil (there are crash reports out there), in which case we'll have to just
+ // live without keep-alive :(
+ if (socketData != nil) {
+ CFSocketNativeHandle socket;
+ CFDataGetBytes(socketData, CFRangeMake(0, sizeof(CFSocketNativeHandle)), (UInt8 *)&socket);
+ CFRelease(socketData);
+
+ int keepAliveOn = 1;
+ if (setsockopt(socket, SOL_SOCKET, SO_KEEPALIVE, &keepAliveOn, sizeof(keepAliveOn)) == -1) {
+ SRFastLog(@"Failed to turn on TCP keepalive for websocket");
+ }
+ }
+ }
+
+ if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {
+
+ NSArray *sslCerts = [_urlRequest FSR_SSLPinnedCertificates];
+ if (sslCerts) {
+ SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust];
+ if (secTrust) {
+ NSInteger numCerts = SecTrustGetCertificateCount(secTrust);
+ for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) {
+ SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i);
+ NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert));
+
+ for (id ref in sslCerts) {
+ SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref;
+ NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert));
+
+ if ([trustedCertData isEqualToData:certData]) {
+ _pinnedCertFound = YES;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!_pinnedCertFound) {
+ dispatch_async(_workQueue, ^{
+ NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : @"Invalid server cert" };
+ [weakSelf _failWithError:[NSError errorWithDomain:@"org.lolrus.SocketRocket" code:23556 userInfo:userInfo]];
+ });
+ return;
+ }
+ }
+ }
+
+ // SRFastLog(@"%@ Got stream event %d", aStream, eventCode);
+ dispatch_async(_workQueue, ^{
+ [weakSelf safeHandleEvent:eventCode stream:aStream];
+ });
+}
+
+- (void)safeHandleEvent:(NSStreamEvent)eventCode stream:(NSStream *)aStream
+{
+ switch (eventCode) {
+ case NSStreamEventOpenCompleted: {
+ SRFastLog(@"NSStreamEventOpenCompleted %@", aStream);
+ if (self.readyState >= SR_CLOSING) {
+ return;
+ }
+
+
+ assert(_readBuffer);
+
+ if (self.readyState == SR_CONNECTING && aStream == _inputStream) {
+ [self didConnect];
+ }
+ [self _pumpWriting];
+ [self _pumpScanner];
+ break;
+ }
+
+ case NSStreamEventErrorOccurred: {
+ SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]);
+ /// TODO specify error better!
+ [self _failWithError:aStream.streamError];
+ _readBufferOffset = 0;
+ [_readBuffer setLength:0];
+ break;
+
+ }
+
+ case NSStreamEventEndEncountered: {
+ [self _pumpScanner];
+ SRFastLog(@"NSStreamEventEndEncountered %@", aStream);
+ if (aStream.streamError) {
+ [self _failWithError:aStream.streamError];
+ } else {
+ dispatch_async(_workQueue, ^{
+ if (self.readyState != SR_CLOSED) {
+ self.readyState = SR_CLOSED;
+ [self _scheduleCleanup];
+ }
+
+ if (!_sentClose && !_failed) {
+ _sentClose = YES;
+ // If we get closed in this state it's probably not clean because we should be sending this when we send messages
+ [self _performDelegateBlock:^{
+ if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) {
+ [self.delegate webSocket:self didCloseWithCode:0 reason:@"Stream end encountered" wasClean:NO];
+ }
+ }];
+ }
+ });
+ }
+
+ break;
+ }
+
+ case NSStreamEventHasBytesAvailable: {
+ SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream);
+ const NSUInteger bufferSize = 2048;
+ uint8_t buffer[bufferSize];
+
+ while (_inputStream.hasBytesAvailable) {
+ NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize];
+
+ if (bytes_read > 0) {
+ [_readBuffer appendBytes:buffer length:bytes_read];
+ } else if (bytes_read < 0) {
+ [self _failWithError:_inputStream.streamError];
+ }
+
+ if (bytes_read != bufferSize) {
+ break;
+ }
+ };
+ [self _pumpScanner];
+ break;
+ }
+
+ case NSStreamEventHasSpaceAvailable: {
+ SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream);
+ [self _pumpWriting];
+ break;
+ }
+
+ default:
+ SRFastLog(@"(default) %@", aStream);
+ break;
+ }
+}
+
+@end
+
+
+@implementation FSRIOConsumer
+
+@synthesize bytesNeeded = _bytesNeeded;
+@synthesize consumer = _scanner;
+@synthesize handler = _handler;
+@synthesize readToCurrentFrame = _readToCurrentFrame;
+@synthesize unmaskBytes = _unmaskBytes;
+
+- (void)setupWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+ _scanner = [scanner copy];
+ _handler = [handler copy];
+ _bytesNeeded = bytesNeeded;
+ _readToCurrentFrame = readToCurrentFrame;
+ _unmaskBytes = unmaskBytes;
+ assert(_scanner || _bytesNeeded);
+}
+
+@end
+
+@implementation FSRIOConsumerPool {
+ NSUInteger _poolSize;
+ NSMutableArray *_bufferedConsumers;
+}
+
+- (id)initWithBufferCapacity:(NSUInteger)poolSize;
+{
+ self = [super init];
+ if (self) {
+ _poolSize = poolSize;
+ _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize];
+ }
+ return self;
+}
+
+- (id)init
+{
+ return [self initWithBufferCapacity:8];
+}
+
+- (FSRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes;
+{
+ FSRIOConsumer *consumer = nil;
+ if (_bufferedConsumers.count) {
+ consumer = [_bufferedConsumers lastObject];
+ [_bufferedConsumers removeLastObject];
+ } else {
+ consumer = [[FSRIOConsumer alloc] init];
+ }
+
+ [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes];
+
+ return consumer;
+}
+
+- (void)returnConsumer:(FSRIOConsumer *)consumer;
+{
+ if (_bufferedConsumers.count < _poolSize) {
+ [_bufferedConsumers addObject:consumer];
+ }
+}
+
+@end
+
+@implementation NSURLRequest (FCertificateAdditions)
+
+- (NSArray *)FSR_SSLPinnedCertificates;
+{
+ return [NSURLProtocol propertyForKey:@"FSR_SSLPinnedCertificates" inRequest:self];
+}
+
+@end
+
+@implementation NSMutableURLRequest (FCertificateAdditions)
+
+- (NSArray *)FSR_SSLPinnedCertificates;
+{
+ return [NSURLProtocol propertyForKey:@"FSR_SSLPinnedCertificates" inRequest:self];
+}
+
+- (void)setFSR_SSLPinnedCertificates:(NSArray *)FSR_SSLPinnedCertificates;
+{
+ [NSURLProtocol setProperty:FSR_SSLPinnedCertificates forKey:@"FSR_SSLPinnedCertificates" inRequest:self];
+}
+
+@end
+
+@implementation NSURL (FSRWebSocket)
+
+- (NSString *)SR_origin;
+{
+ NSString *scheme = [self.scheme lowercaseString];
+
+ if ([scheme isEqualToString:@"wss"]) {
+ scheme = @"https";
+ } else if ([scheme isEqualToString:@"ws"]) {
+ scheme = @"http";
+ }
+
+ if (self.port) {
+ return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port];
+ } else {
+ return [NSString stringWithFormat:@"%@://%@/", scheme, self.host];
+ }
+}
+
+@end
+
+// #define SR_ENABLE_LOG
+
+static inline void SRFastLog(NSString *format, ...) {
+#ifdef SR_ENABLE_LOG
+ __block va_list arg_list;
+ va_start (arg_list, format);
+
+ NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list];
+
+ va_end(arg_list);
+
+ NSLog(@"[SR] %@", formattedString);
+#endif
+}
+
+
+#ifdef HAS_ICU
+
+static inline int32_t validate_dispatch_data_partial_string(NSData *data) {
+
+ const void * contents = [data bytes];
+ long size = [data length];
+
+ const uint8_t *str = (const uint8_t *)contents;
+
+
+ UChar32 codepoint = 1;
+ int32_t offset = 0;
+ int32_t lastOffset = 0;
+ while(offset < size && codepoint > 0) {
+ lastOffset = offset;
+ U8_NEXT(str, offset, size, codepoint);
+ }
+
+ if (codepoint == -1) {
+ // Check to see if the last byte is valid or whether it was just continuing
+ if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) {
+
+ size = -1;
+ } else {
+ uint8_t leadByte = str[lastOffset];
+ U8_MASK_LEAD_BYTE(leadByte, U8_COUNT_TRAIL_BYTES(leadByte));
+
+ for (int i = lastOffset + 1; i < offset; i++) {
+
+ if (U8_IS_SINGLE(str[i]) || U8_IS_LEAD(str[i]) || !U8_IS_TRAIL(str[i])) {
+ size = -1;
+ }
+ }
+
+ if (size != -1) {
+ size = lastOffset;
+ }
+ }
+ }
+
+ if (size != -1 && ![[NSString alloc] initWithBytesNoCopy:(char *)[data bytes] length:size encoding:NSUTF8StringEncoding freeWhenDone:NO]) {
+ size = -1;
+ }
+
+ return (int32_t)size;
+}
+
+#else
+
+// This is a hack, and probably not optimal
+static inline int32_t validate_dispatch_data_partial_string(NSData *data) {
+ static const int maxCodepointSize = 3;
+
+ for (int i = 0; i < maxCodepointSize; i++) {
+ NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO];
+ if (str) {
+ return (int)(data.length - i);
+ }
+ }
+
+ return -1;
+}
+
+#endif
+
+static _FSRRunLoopThread *networkThread = nil;
+static NSRunLoop *networkRunLoop = nil;
+
+@implementation NSRunLoop (FSRWebSocket)
+
++ (NSRunLoop *)FSR_networkRunLoop {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ networkThread = [[_FSRRunLoopThread alloc] init];
+ networkThread.name = @"com.squareup.SocketRocket.NetworkThread";
+ [networkThread start];
+ networkRunLoop = networkThread.runLoop;
+ });
+
+ return networkRunLoop;
+}
+
+@end
+
+
+@implementation _FSRRunLoopThread {
+ dispatch_group_t _waitGroup;
+}
+
+@synthesize runLoop = _runLoop;
+
+- (void)dealloc
+{
+ sr_dispatch_release(_waitGroup);
+}
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ _waitGroup = dispatch_group_create();
+ dispatch_group_enter(_waitGroup);
+ }
+ return self;
+}
+
+
+/**
+ * This is the main method of the thread on which the socket events are scheduled in a run loop.
+ */
+- (void)main;
+{
+ @autoreleasepool {
+ _runLoop = [NSRunLoop currentRunLoop];
+ dispatch_group_leave(_waitGroup);
+
+ // Add an empty run loop source to prevent runloop from spinning.
+ CFRunLoopSourceContext sourceCtx = {
+ .version = 0,
+ .info = NULL,
+ .retain = NULL,
+ .release = NULL,
+ .copyDescription = NULL,
+ .equal = NULL,
+ .hash = NULL,
+ .schedule = NULL,
+ .cancel = NULL,
+ .perform = NULL
+ };
+ CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &sourceCtx);
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
+ CFRelease(source);
+
+ while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
+
+ }
+ assert(NO);
+ }
+}
+
+- (NSRunLoop *)runLoop;
+{
+ dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER);
+ return _runLoop;
+}
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h
new file mode 100644
index 0000000..bac393b
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.h
@@ -0,0 +1,23 @@
+//
+// Copyright 2012 Square Inc.
+//
+// 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 FSRUtilities : NSObject
+
++ (NSString *)base64EncodedStringFromData:(NSData *)data;
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m
new file mode 100644
index 0000000..2be1d84
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/NSData+SRB64Additions.m
@@ -0,0 +1,37 @@
+//
+// Copyright 2012 Square Inc.
+//
+// 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 "NSData+SRB64Additions.h"
+#import "fbase64.h"
+
+@implementation FSRUtilities
+
++ (NSString *)base64EncodedStringFromData:(NSData *)data {
+ size_t buffer_size = ((data.length * 3 + 2) / 2);
+
+ char *buffer = (char *)malloc(buffer_size);
+
+ int len = f_b64_ntop(data.bytes, data.length, buffer, buffer_size);
+
+ if (len == -1) {
+ free(buffer);
+ return nil;
+ } else{
+ return [[NSString alloc] initWithBytesNoCopy:buffer length:len encoding:NSUTF8StringEncoding freeWhenDone:YES];
+ }
+}
+
+@end
diff --git a/Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb3 b/Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb3
new file mode 100644
index 0000000..152c47c
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/aa2297808c225710e267afece4439c256f6efdb3
@@ -0,0 +1,3 @@
+Fri Aug 3 15:45:39 PDT 2012
+Github commit: aa2297808c225710e267afece4439c256f6efdb3
+
diff --git a/Firebase/Database/third_party/SocketRocket/fbase64.c b/Firebase/Database/third_party/SocketRocket/fbase64.c
new file mode 100644
index 0000000..1750673
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/fbase64.c
@@ -0,0 +1,318 @@
+/* $OpenBSD: base64.c,v 1.5 2006/10/21 09:55:03 otto Exp $ */
+
+/*
+ * Copyright (c) 1996 by Internet Software Consortium.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS
+ * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
+ * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+ * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+ * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+ * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+ * SOFTWARE.
+ */
+
+/*
+ * Portions Copyright (c) 1995 by International Business Machines, Inc.
+ *
+ * International Business Machines, Inc. (hereinafter called IBM) grants
+ * permission under its copyrights to use, copy, modify, and distribute this
+ * Software with or without fee, provided that the above copyright notice and
+ * all paragraphs of this notice appear in all copies, and that the name of IBM
+ * not be used in connection with the marketing of any product incorporating
+ * the Software or modifications thereof, without specific, written prior
+ * permission.
+ *
+ * To the extent it has a right to do so, IBM grants an immunity from suit
+ * under its patents, if any, for the use, sale or manufacture of products to
+ * the extent that such products are used for performing Domain Name System
+ * dynamic updates in TCP/IP networks by means of the Software. No immunity is
+ * granted for any product per se or for any other function of any product.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES,
+ * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL,
+ * DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN
+ * IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ */
+
+/* OPENBSD ORIGINAL: lib/libc/net/base64.c */
+
+
+//
+// Distributed with modifications by Firebase ( https://www.firebase.com )
+//
+
+#if (!defined(HAVE_B64_NTOP) && !defined(HAVE___B64_NTOP)) || (!defined(HAVE_B64_PTON) && !defined(HAVE___B64_PTON))
+
+#include <sys/types.h>
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <ctype.h>
+#include <stdio.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "fbase64.h"
+
+static const char Base64[] =
+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+static const char Pad64 = '=';
+
+/* (From RFC1521 and draft-ietf-dnssec-secext-03.txt)
+ The following encoding technique is taken from RFC 1521 by Borenstein
+ and Freed. It is reproduced here in a slightly edited form for
+ convenience.
+
+ A 65-character subset of US-ASCII is used, enabling 6 bits to be
+ represented per printable character. (The extra 65th character, "=",
+ is used to signify a special processing function.)
+
+ The encoding process represents 24-bit groups of input bits as output
+ strings of 4 encoded characters. Proceeding from left to right, a
+ 24-bit input group is formed by concatenating 3 8-bit input groups.
+ These 24 bits are then treated as 4 concatenated 6-bit groups, each
+ of which is translated into a single digit in the base64 alphabet.
+
+ Each 6-bit group is used as an index into an array of 64 printable
+ characters. The character referenced by the index is placed in the
+ output string.
+
+ Table 1: The Base64 Alphabet
+
+ Value Encoding Value Encoding Value Encoding Value Encoding
+ 0 A 17 R 34 i 51 z
+ 1 B 18 S 35 j 52 0
+ 2 C 19 T 36 k 53 1
+ 3 D 20 U 37 l 54 2
+ 4 E 21 V 38 m 55 3
+ 5 F 22 W 39 n 56 4
+ 6 G 23 X 40 o 57 5
+ 7 H 24 Y 41 p 58 6
+ 8 I 25 Z 42 q 59 7
+ 9 J 26 a 43 r 60 8
+ 10 K 27 b 44 s 61 9
+ 11 L 28 c 45 t 62 +
+ 12 M 29 d 46 u 63 /
+ 13 N 30 e 47 v
+ 14 O 31 f 48 w (pad) =
+ 15 P 32 g 49 x
+ 16 Q 33 h 50 y
+
+ Special processing is performed if fewer than 24 bits are available
+ at the end of the data being encoded. A full encoding quantum is
+ always completed at the end of a quantity. When fewer than 24 input
+ bits are available in an input group, zero bits are added (on the
+ right) to form an integral number of 6-bit groups. Padding at the
+ end of the data is performed using the '=' character.
+
+ Since all base64 input is an integral number of octets, only the
+ -------------------------------------------------
+ following cases can arise:
+
+ (1) the final quantum of encoding input is an integral
+ multiple of 24 bits; here, the final unit of encoded
+ output will be an integral multiple of 4 characters
+ with no "=" padding,
+ (2) the final quantum of encoding input is exactly 8 bits;
+ here, the final unit of encoded output will be two
+ characters followed by two "=" padding characters, or
+ (3) the final quantum of encoding input is exactly 16 bits;
+ here, the final unit of encoded output will be three
+ characters followed by one "=" padding character.
+ */
+
+#if !defined(HAVE_B64_NTOP) && !defined(HAVE___B64_NTOP)
+int
+f_b64_ntop(u_char const *src, size_t srclength, char *target, size_t targsize)
+{
+ size_t datalength = 0;
+ u_char input[3];
+ u_char output[4];
+ u_int i;
+
+ while (2 < srclength) {
+ input[0] = *src++;
+ input[1] = *src++;
+ input[2] = *src++;
+ srclength -= 3;
+
+ output[0] = input[0] >> 2;
+ output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+ output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+ output[3] = input[2] & 0x3f;
+
+ if (datalength + 4 > targsize)
+ return (-1);
+ target[datalength++] = Base64[output[0]];
+ target[datalength++] = Base64[output[1]];
+ target[datalength++] = Base64[output[2]];
+ target[datalength++] = Base64[output[3]];
+ }
+
+ /* Now we worry about padding. */
+ if (0 != srclength) {
+ /* Get what's left. */
+ input[0] = input[1] = input[2] = '\0';
+ for (i = 0; i < srclength; i++)
+ input[i] = *src++;
+
+ output[0] = input[0] >> 2;
+ output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+ output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+
+ if (datalength + 4 > targsize)
+ return (-1);
+ target[datalength++] = Base64[output[0]];
+ target[datalength++] = Base64[output[1]];
+ if (srclength == 1)
+ target[datalength++] = Pad64;
+ else
+ target[datalength++] = Base64[output[2]];
+ target[datalength++] = Pad64;
+ }
+ if (datalength >= targsize)
+ return (-1);
+ target[datalength] = '\0'; /* Returned value doesn't count \0. */
+ return (int)(datalength);
+}
+#endif /* !defined(HAVE_B64_NTOP) && !defined(HAVE___B64_NTOP) */
+
+#if !defined(HAVE_B64_PTON) && !defined(HAVE___B64_PTON)
+
+/* skips all whitespace anywhere.
+ converts characters, four at a time, starting at (or after)
+ src from base - 64 numbers into three 8 bit bytes in the target area.
+ it returns the number of data bytes stored at the target, or -1 on error.
+ */
+
+int
+f_b64_pton(char const *src, u_char *target, size_t targsize)
+{
+ u_int tarindex, state;
+ int ch;
+ char *pos;
+
+ state = 0;
+ tarindex = 0;
+
+ while ((ch = *src++) != '\0') {
+ if (isspace(ch)) /* Skip whitespace anywhere. */
+ continue;
+
+ if (ch == Pad64)
+ break;
+
+ pos = strchr(Base64, ch);
+ if (pos == 0) /* A non-base64 character. */
+ return (-1);
+
+ switch (state) {
+ case 0:
+ if (target) {
+ if (tarindex >= targsize)
+ return (-1);
+ target[tarindex] = (pos - Base64) << 2;
+ }
+ state = 1;
+ break;
+ case 1:
+ if (target) {
+ if (tarindex + 1 >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64) >> 4;
+ target[tarindex+1] = ((pos - Base64) & 0x0f)
+ << 4 ;
+ }
+ tarindex++;
+ state = 2;
+ break;
+ case 2:
+ if (target) {
+ if (tarindex + 1 >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64) >> 2;
+ target[tarindex+1] = ((pos - Base64) & 0x03)
+ << 6;
+ }
+ tarindex++;
+ state = 3;
+ break;
+ case 3:
+ if (target) {
+ if (tarindex >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64);
+ }
+ tarindex++;
+ state = 0;
+ break;
+ }
+ }
+
+ /*
+ * We are done decoding Base-64 chars. Let's see if we ended
+ * on a byte boundary, and/or with erroneous trailing characters.
+ */
+
+ if (ch == Pad64) { /* We got a pad char. */
+ ch = *src++; /* Skip it, get next. */
+ switch (state) {
+ case 0: /* Invalid = in first position */
+ case 1: /* Invalid = in second position */
+ return (-1);
+
+ case 2: /* Valid, means one byte of info */
+ /* Skip any number of spaces. */
+ for (; ch != '\0'; ch = *src++)
+ if (!isspace(ch))
+ break;
+ /* Make sure there is another trailing = sign. */
+ if (ch != Pad64)
+ return (-1);
+ ch = *src++; /* Skip the = */
+ /* Fall through to "single trailing =" case. */
+ /* FALLTHROUGH */
+
+ case 3: /* Valid, means two bytes of info */
+ /*
+ * We know this char is an =. Is there anything but
+ * whitespace after it?
+ */
+ for (; ch != '\0'; ch = *src++)
+ if (!isspace(ch))
+ return (-1);
+
+ /*
+ * Now make sure for cases 2 and 3 that the "extra"
+ * bits that slopped past the last full byte were
+ * zeros. If we don't check them, they become a
+ * subliminal channel.
+ */
+ if (target && target[tarindex] != 0)
+ return (-1);
+ }
+ } else {
+ /*
+ * We ended by seeing the end of the string. Make sure we
+ * have no partial bytes lying around.
+ */
+ if (state != 0)
+ return (-1);
+ }
+
+ return (tarindex);
+}
+
+#endif /* !defined(HAVE_B64_PTON) && !defined(HAVE___B64_PTON) */
+#endif \ No newline at end of file
diff --git a/Firebase/Database/third_party/SocketRocket/fbase64.h b/Firebase/Database/third_party/SocketRocket/fbase64.h
new file mode 100644
index 0000000..a9c55c9
--- /dev/null
+++ b/Firebase/Database/third_party/SocketRocket/fbase64.h
@@ -0,0 +1,33 @@
+// Copyright 2012 Square Inc.
+//
+// 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.
+//
+
+#ifndef FSocketRocket_base64_h
+#define FSocketRocket_base64_h
+
+#include <sys/types.h>
+
+extern int
+f_b64_ntop(u_char const *src,
+ size_t srclength,
+ char *target,
+ size_t targsize);
+
+extern int
+f_b64_pton(char const *src,
+ u_char *target,
+ size_t targsize);
+
+
+#endif
diff --git a/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h
new file mode 100644
index 0000000..c0baa22
--- /dev/null
+++ b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.h
@@ -0,0 +1,105 @@
+//
+// APLevelDB.h
+//
+// Created by Adam Preble on 1/23/12.
+// Copyright (c) 2012 Adam Preble. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+#import <Foundation/Foundation.h>
+
+extern NSString * const APLevelDBErrorDomain;
+
+@class APLevelDBIterator;
+@protocol APLevelDBWriteBatch;
+
+@interface APLevelDB : NSObject
+
+@property (nonatomic, readonly, strong) NSString *path;
+
++ (APLevelDB *)levelDBWithPath:(NSString *)path error:(NSError *__autoreleasing*)errorOut;
+- (void)close;
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key;
+- (BOOL)setString:(NSString *)str forKey:(NSString *)key;
+
+- (NSData *)dataForKey:(NSString *)key;
+- (NSString *)stringForKey:(NSString *)key;
+
+- (BOOL)removeKey:(NSString *)key;
+
+- (NSArray *)allKeys;
+
+- (void)enumerateKeys:(void (^)(NSString *key, BOOL *stop))block;
+- (void)enumerateKeysWithPrefix:(NSString *)prefix usingBlock:(void (^)(NSString *key, BOOL *stop))block;
+
+- (void)enumerateKeysAndValuesAsStrings:(void (^)(NSString *key, NSString *value, BOOL *stop))block;
+- (void)enumerateKeysWithPrefix:(NSString *)prefix asStrings:(void (^)(NSString *key, NSString *value, BOOL *stop))block;
+
+- (void)enumerateKeysAndValuesAsData:(void (^)(NSString *key, NSData *value, BOOL *stop))block;
+- (void)enumerateKeysWithPrefix:(NSString *)prefix asData:(void (^)(NSString *key, NSData *value, BOOL *stop))block;
+
+- (NSUInteger)approximateSizeFrom:(NSString *)from to:(NSString *)to;
+- (NSUInteger)exactSizeFrom:(NSString *)from to:(NSString *)to;
+
+// Objective-C Subscripting Support:
+// The database object supports subscripting for string-string and string-data key-value access and assignment.
+// Examples:
+// db[@"key"] = @"value";
+// db[@"key"] = [NSData data];
+// NSString *s = db[@"key"];
+// An NSInvalidArgumentException is raised if the key is not an NSString, or if the assigned object is not an
+// instance of NSString or NSData.
+- (id)objectForKeyedSubscript:(id)key;
+- (void)setObject:(id)object forKeyedSubscript:(id<NSCopying>)key;
+
+// Batch write/atomic update support:
+- (id<APLevelDBWriteBatch>)beginWriteBatch;
+
+@end
+
+
+@interface APLevelDBIterator : NSObject
+
++ (id)iteratorWithLevelDB:(APLevelDB *)db;
+
+// Designated initializer:
+- (id)initWithLevelDB:(APLevelDB *)db;
+
+- (BOOL)seekToKey:(NSString *)key;
+- (NSString *)nextKey;
+- (NSString *)key;
+- (NSString *)valueAsString;
+- (NSData *)valueAsData;
+
+@end
+
+
+@protocol APLevelDBWriteBatch <NSObject>
+
+- (void)setData:(NSData *)data forKey:(NSString *)key;
+- (void)setString:(NSString *)str forKey:(NSString *)key;
+
+- (void)removeKey:(NSString *)key;
+
+// Remove all of the buffered sets and removes:
+- (void)clear;
+- (BOOL)commit;
+
+@end
diff --git a/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm
new file mode 100644
index 0000000..cdecce6
--- /dev/null
+++ b/Firebase/Database/third_party/Wrap-leveldb/APLevelDB.mm
@@ -0,0 +1,500 @@
+//
+// APLevelDB.m
+//
+// Created by Adam Preble on 1/23/12.
+// Copyright (c) 2012 Adam Preble. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+//
+// Portions of APLevelDB are based on LevelDB-ObjC:
+// https://github.com/hoisie/LevelDB-ObjC
+// Specifically the SliceFromString/StringFromSlice macros, and the structure of
+// the enumeration methods. License for those potions follows:
+//
+// Copyright (c) 2011 Pave Labs
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#import "APLevelDB.h"
+
+#import "leveldb/db.h"
+#import "leveldb/options.h"
+#import "leveldb/write_batch.h"
+
+NSString * const APLevelDBErrorDomain = @"APLevelDBErrorDomain";
+
+#define SliceFromString(_string_) (leveldb::Slice((char *)[_string_ UTF8String], [_string_ lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))
+#define StringFromSlice(_slice_) ([[NSString alloc] initWithBytes:_slice_.data() length:_slice_.size() encoding:NSUTF8StringEncoding])
+
+
+@interface APLevelDBWriteBatch : NSObject <APLevelDBWriteBatch> {
+ @package
+ leveldb::WriteBatch _batch;
+}
+
+@property (nonatomic, strong) APLevelDB *levelDB;
+
+- (id)initWithLevelDB:(APLevelDB *)levelDB;
+@end
+
+
+#pragma mark - APLevelDB
+
+@interface APLevelDB () {
+ leveldb::DB *_db;
+ leveldb::ReadOptions _readOptions;
+ leveldb::WriteOptions _writeOptions;
+}
+- (id)initWithPath:(NSString *)path error:(NSError **)errorOut;
++ (leveldb::Options)defaultCreateOptions;
+@property (nonatomic, readonly) leveldb::DB *db;
+@end
+
+
+@implementation APLevelDB
+
+@synthesize path = _path;
+@synthesize db = _db;
+
++ (APLevelDB *)levelDBWithPath:(NSString *)path error:(NSError *__autoreleasing *)errorOut
+{
+ return [[APLevelDB alloc] initWithPath:path error:errorOut];
+}
+
+- (id)initWithPath:(NSString *)path error:(NSError *__autoreleasing *)errorOut
+{
+ if ((self = [super init]))
+ {
+ _path = path;
+
+ leveldb::Options options = [[self class] defaultCreateOptions];
+
+ leveldb::Status status = leveldb::DB::Open(options, [_path UTF8String], &_db);
+
+ if (!status.ok())
+ {
+ if (errorOut)
+ {
+ NSString *statusString = [[NSString alloc] initWithCString:status.ToString().c_str() encoding:NSUTF8StringEncoding];
+ *errorOut = [NSError errorWithDomain:APLevelDBErrorDomain
+ code:0
+ userInfo:[NSDictionary dictionaryWithObjectsAndKeys:statusString, NSLocalizedDescriptionKey, nil]];
+ }
+ return nil;
+ }
+
+ _writeOptions.sync = false;
+ }
+ return self;
+}
+
+- (void)close {
+ if (_db != NULL) {
+ delete _db;
+ _db = NULL;
+ }
+}
+
+- (void)dealloc
+{
+ if (_db != NULL) {
+ delete _db;
+ _db = NULL;
+ }
+}
+
++ (leveldb::Options)defaultCreateOptions
+{
+ leveldb::Options options;
+ options.create_if_missing = true;
+ return options;
+}
+
+- (BOOL)setData:(NSData *)data forKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = leveldb::Slice((const char *)[data bytes], (size_t)[data length]);
+ leveldb::Status status = _db->Put(_writeOptions, keySlice, valueSlice);
+ return (status.ok() == true);
+}
+
+- (BOOL)setString:(NSString *)str forKey:(NSString *)key
+{
+ // This could have been based on
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = SliceFromString(str);
+ leveldb::Status status = _db->Put(_writeOptions, keySlice, valueSlice);
+ return (status.ok() == true);
+}
+
+- (NSData *)dataForKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ std::string valueCPPString;
+ leveldb::Status status = _db->Get(_readOptions, keySlice, &valueCPPString);
+
+ if (!status.ok())
+ return nil;
+ else
+ return [NSData dataWithBytes:valueCPPString.data() length:valueCPPString.size()];
+}
+
+- (NSString *)stringForKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ std::string valueCPPString;
+ leveldb::Status status = _db->Get(_readOptions, keySlice, &valueCPPString);
+
+ // We assume (dangerously?) UTF-8 string encoding:
+ if (!status.ok())
+ return nil;
+ else
+ return [[NSString alloc] initWithBytes:valueCPPString.data() length:valueCPPString.size() encoding:NSUTF8StringEncoding];
+}
+
+- (BOOL)removeKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Status status = _db->Delete(_writeOptions, keySlice);
+ return (status.ok() == true);
+}
+
+- (NSArray *)allKeys
+{
+ NSMutableArray *keys = [NSMutableArray array];
+ [self enumerateKeys:^(NSString *key, BOOL *stop) {
+ [keys addObject:key];
+ }];
+ return keys;
+}
+
+- (void)enumerateKeysAndValuesAsStrings:(void (^)(NSString *key, NSString *value, BOOL *stop))block
+{
+ [self enumerateKeysWithPrefix:@"" asStrings:block];
+}
+
+- (void)enumerateKeysWithPrefix:(NSString *)prefixString asStrings:(void (^)(NSString *, NSString *, BOOL *))block
+{
+ @autoreleasepool {
+ BOOL stop = NO;
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ leveldb::Slice prefix = SliceFromString(prefixString);
+ for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
+ leveldb::Slice key = iter->key(), value = iter->value();
+ if (key.starts_with(prefix)) {
+ NSString *k = StringFromSlice(key);
+ NSString *v = [[NSString alloc] initWithBytes:value.data() length:value.size() encoding:NSUTF8StringEncoding];
+ block(k, v, &stop);
+ if (stop)
+ break;
+ } else {
+ break;
+ }
+ }
+
+ delete iter;
+ }
+}
+
+- (void)enumerateKeys:(void (^)(NSString *key, BOOL *stop))block
+{
+ [self enumerateKeysWithPrefix:@"" usingBlock:block];
+}
+
+- (void)enumerateKeysWithPrefix:(NSString *)prefixString usingBlock:(void (^)(NSString *key, BOOL *stop))block;
+{
+ @autoreleasepool {
+ BOOL stop = NO;
+ leveldb::Slice prefix = SliceFromString(prefixString);
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
+ leveldb::Slice key = iter->key();
+ if (key.starts_with(prefix)) {
+ NSString *k = StringFromSlice(key);
+ block(k, &stop);
+ if (stop)
+ break;
+ } else {
+ break;
+ }
+ }
+
+ delete iter;
+ }
+}
+
+- (void)enumerateKeysAndValuesAsData:(void (^)(NSString *key, NSData *data, BOOL *stop))block
+{
+ [self enumerateKeysWithPrefix:@"" asData:block];
+}
+
+- (void)enumerateKeysWithPrefix:(NSString *)prefixString asData:(void (^)(NSString *, NSData *, BOOL *))block
+{
+ @autoreleasepool {
+ BOOL stop = NO;
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ leveldb::Slice prefix = SliceFromString(prefixString);
+ for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
+ leveldb::Slice key = iter->key(), value = iter->value();
+ if (key.starts_with(prefix)) {
+ NSString *k = StringFromSlice(key);
+ NSData *data = [NSData dataWithBytes:value.data() length:value.size()];
+ block(k, data, &stop);
+ if (stop)
+ break;
+ } else {
+ break;
+ }
+ }
+
+ delete iter;
+ }
+}
+
+- (NSUInteger)exactSizeFrom:(NSString *)from to:(NSString *)to {
+ NSUInteger size = 0;
+ leveldb::Iterator* iter = _db->NewIterator(leveldb::ReadOptions());
+ leveldb::Slice fromSlice = SliceFromString(from);
+ leveldb::Slice toSlice = SliceFromString(to);
+ iter->Seek(fromSlice);
+ while (iter->Valid() && iter->key().compare(toSlice) <= 0) {
+ size += iter->value().size();
+ iter->Next();
+ }
+ delete iter;
+ return size;
+}
+
+
+- (NSUInteger)approximateSizeFrom:(NSString *)from to:(NSString *)to {
+ leveldb::Range ranges[1];
+ leveldb::Slice fromSlice = SliceFromString(from);
+ leveldb::Slice toSlice = SliceFromString(to);
+ ranges[0] = leveldb::Range(fromSlice, toSlice);
+ uint64_t sizes[1];
+ _db->GetApproximateSizes(ranges, 1, sizes);
+ return (NSUInteger)sizes[0];
+}
+
+#pragma mark - Subscripting Support
+
+- (id)objectForKeyedSubscript:(id)key
+{
+ if (![key respondsToSelector: @selector(componentsSeparatedByString:)])
+ {
+ [NSException raise:NSInvalidArgumentException format:@"key must be an NSString"];
+ }
+ return [self stringForKey:key];
+}
+- (void)setObject:(id)thing forKeyedSubscript:(id<NSCopying>)key
+{
+ id idKey = (id) key;
+ if (![idKey respondsToSelector: @selector(componentsSeparatedByString:)])
+ {
+ [NSException raise:NSInvalidArgumentException format:@"key must be NSString or NSData"];
+ }
+
+ if ([thing respondsToSelector:@selector(componentsSeparatedByString:)])
+ [self setString:thing forKey:(NSString *)key];
+ else if ([thing respondsToSelector:@selector(subdataWithRange:)])
+ [self setData:thing forKey:(NSString *)key];
+ else
+ [NSException raise:NSInvalidArgumentException format:@"object must be NSString or NSData"];
+}
+
+#pragma mark - Atomic Updates
+
+- (id<APLevelDBWriteBatch>)beginWriteBatch
+{
+ APLevelDBWriteBatch *batch = [[APLevelDBWriteBatch alloc] initWithLevelDB:self];
+ return batch;
+}
+
+- (BOOL)commitWriteBatch:(id<APLevelDBWriteBatch>)theBatch
+{
+ if (!theBatch)
+ return NO;
+
+ APLevelDBWriteBatch *batch = theBatch;
+
+ leveldb::Status status;
+ status = _db->Write(_writeOptions, &batch->_batch);
+ return (status.ok() == true);
+}
+
+@end
+
+
+#pragma mark - APLevelDBIterator
+
+@interface APLevelDBIterator () {
+ leveldb::Iterator *_iter;
+}
+
+@property (nonatomic, strong) APLevelDB *levelDB;
+@end
+
+
+
+@implementation APLevelDBIterator
+
++ (id)iteratorWithLevelDB:(APLevelDB *)db
+{
+ APLevelDBIterator *iter = [[[self class] alloc] initWithLevelDB:db];
+ return iter;
+}
+
+- (id)initWithLevelDB:(APLevelDB *)db
+{
+ if ((self = [super init]))
+ {
+ // Hold on to the database so it doesn't get deallocated before the iterator is deallocated
+ self->_levelDB = db;
+ _iter = db.db->NewIterator(leveldb::ReadOptions());
+ _iter->SeekToFirst();
+ if (!_iter->Valid())
+ return nil;
+ }
+ return self;
+}
+
+- (id)init
+{
+ [NSException raise:@"BadInitializer" format:@"Use the designated initializer, -initWithLevelDB:, instead."];
+ return nil;
+}
+
+- (void)dealloc
+{
+ self->_levelDB = nil;
+ delete _iter;
+ _iter = NULL;
+}
+
+- (BOOL)seekToKey:(NSString *)key
+{
+ leveldb::Slice target = SliceFromString(key);
+ _iter->Seek(target);
+ return _iter->Valid() == true;
+}
+
+- (void)seekToFirst
+{
+ _iter->SeekToFirst();
+}
+
+- (void)seekToLast
+{
+ _iter->SeekToLast();
+}
+
+- (NSString *)nextKey
+{
+ _iter->Next();
+ return [self key];
+}
+
+- (NSString *)key
+{
+ if (_iter->Valid() == false)
+ return nil;
+ leveldb::Slice value = _iter->key();
+ return StringFromSlice(value);
+}
+
+- (NSString *)valueAsString
+{
+ if (_iter->Valid() == false)
+ return nil;
+ leveldb::Slice value = _iter->value();
+ return StringFromSlice(value);
+}
+
+- (NSData *)valueAsData
+{
+ if (_iter->Valid() == false)
+ return nil;
+ leveldb::Slice value = _iter->value();
+ return [NSData dataWithBytes:value.data() length:value.size()];
+}
+
+@end
+
+
+
+#pragma mark - APLevelDBWriteBatch
+
+@implementation APLevelDBWriteBatch
+
+- (id)initWithLevelDB:(APLevelDB *)levelDB {
+ self = [super init];
+ if (self != nil) {
+ self->_levelDB = levelDB;
+ }
+ return self;
+}
+
+- (void)setData:(NSData *)data forKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = leveldb::Slice((const char *)[data bytes], (size_t)[data length]);
+ _batch.Put(keySlice, valueSlice);
+}
+- (void)setString:(NSString *)str forKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ leveldb::Slice valueSlice = SliceFromString(str);
+ _batch.Put(keySlice, valueSlice);
+}
+
+- (void)removeKey:(NSString *)key
+{
+ leveldb::Slice keySlice = SliceFromString(key);
+ _batch.Delete(keySlice);
+}
+
+- (void)clear
+{
+ _batch.Clear();
+}
+
+- (BOOL)commit {
+ return [self.levelDB commitWriteBatch:self];
+}
+
+@end
+