aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Gil <mcg@google.com>2017-10-03 08:55:22 -0700
committerGravatar GitHub <noreply@github.com>2017-10-03 08:55:22 -0700
commitbde743ed25166a0b320ae157bfb1d68064f531c9 (patch)
tree4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884
parentbf550507ffa8beee149383a5bf1e2363bccefbb4 (diff)
Release 4.3.0 (#327)
Initial release of Firestore at 0.8.0 Bump FirebaseCommunity to 0.1.3
-rw-r--r--Firebase/Core/FIRLogger.m1
-rw-r--r--Firebase/Core/Private/FIRLogger.h1
-rw-r--r--FirebaseCommunity.podspec2
-rw-r--r--Firestore/Example/Firestore.xcodeproj/project.pbxproj1700
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme111
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme113
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme71
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme71
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme91
-rw-r--r--Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Firestore/Example/Firestore/Base.lproj/Main.storyboard27
-rw-r--r--Firestore/Example/Firestore/FIRAppDelegate.h23
-rw-r--r--Firestore/Example/Firestore/FIRAppDelegate.m57
-rw-r--r--Firestore/Example/Firestore/FIRViewController.h21
-rw-r--r--Firestore/Example/Firestore/FIRViewController.m35
-rw-r--r--Firestore/Example/Firestore/Firestore-Info.plist49
-rw-r--r--Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json93
-rw-r--r--Firestore/Example/Firestore/en.lproj/InfoPlist.strings2
-rw-r--r--Firestore/Example/Firestore/main.m24
-rw-r--r--Firestore/Example/Podfile22
-rw-r--r--Firestore/Example/SwiftBuildTest/main.swift284
-rw-r--r--Firestore/Example/Tests/API/FIRGeoPointTests.m67
-rw-r--r--Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m59
-rw-r--r--Firestore/Example/Tests/Core/FSTEventManagerTests.m163
-rw-r--r--Firestore/Example/Tests/Core/FSTQueryListenerTests.m487
-rw-r--r--Firestore/Example/Tests/Core/FSTQueryTests.m577
-rw-r--r--Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h32
-rw-r--r--Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m94
-rw-r--r--Firestore/Example/Tests/Core/FSTTimestampTests.m88
-rw-r--r--Firestore/Example/Tests/Core/FSTViewSnapshotTest.m141
-rw-r--r--Firestore/Example/Tests/Core/FSTViewTests.m618
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRCursorTests.m195
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m741
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRFieldsTests.m223
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m129
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRQueryTests.m197
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m183
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRTypeTests.m79
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRValidationTests.m560
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m313
-rw-r--r--Firestore/Example/Tests/Integration/CAcert.pem0
-rw-r--r--Firestore/Example/Tests/Integration/FSTDatastoreTests.m239
-rw-r--r--Firestore/Example/Tests/Integration/FSTSmokeTests.m129
-rw-r--r--Firestore/Example/Tests/Integration/FSTTransactionTests.m541
-rw-r--r--Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m111
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm361
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m45
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm158
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m54
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm78
-rw-r--r--Firestore/Example/Tests/Local/FSTLocalSerializerTests.m181
-rw-r--r--Firestore/Example/Tests/Local/FSTLocalStoreTests.h38
-rw-r--r--Firestore/Example/Tests/Local/FSTLocalStoreTests.m795
-rw-r--r--Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m44
-rw-r--r--Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m42
-rw-r--r--Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m54
-rw-r--r--Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m49
-rw-r--r--Firestore/Example/Tests/Local/FSTMutationQueueTests.h38
-rw-r--r--Firestore/Example/Tests/Local/FSTMutationQueueTests.m511
-rw-r--r--Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h40
-rw-r--r--Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m72
-rw-r--r--Firestore/Example/Tests/Local/FSTQueryCacheTests.h47
-rw-r--r--Firestore/Example/Tests/Local/FSTQueryCacheTests.m375
-rw-r--r--Firestore/Example/Tests/Local/FSTReferenceSetTests.m84
-rw-r--r--Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h39
-rw-r--r--Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m151
-rw-r--r--Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m113
-rw-r--r--Firestore/Example/Tests/Local/FSTWriteGroupTests.mm121
-rw-r--r--Firestore/Example/Tests/Model/FSTDatabaseIDTests.m45
-rw-r--r--Firestore/Example/Tests/Model/FSTDocumentKeyTests.m60
-rw-r--r--Firestore/Example/Tests/Model/FSTDocumentSetTests.m142
-rw-r--r--Firestore/Example/Tests/Model/FSTDocumentTests.m101
-rw-r--r--Firestore/Example/Tests/Model/FSTFieldValueTests.m576
-rw-r--r--Firestore/Example/Tests/Model/FSTMutationTests.m216
-rw-r--r--Firestore/Example/Tests/Model/FSTPathTests.m196
-rw-r--r--Firestore/Example/Tests/Remote/FSTDatastoreTests.m58
-rw-r--r--Firestore/Example/Tests/Remote/FSTRemoteEventTests.m556
-rw-r--r--Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m794
-rw-r--r--Firestore/Example/Tests/Remote/FSTStreamTests.m139
-rw-r--r--Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h40
-rw-r--r--Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m54
-rw-r--r--Firestore/Example/Tests/Remote/FSTWatchChangeTests.m66
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m43
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m42
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTMockDatastore.h68
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTMockDatastore.m344
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSpecTests.h46
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSpecTests.m642
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h248
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m291
-rw-r--r--Firestore/Example/Tests/SpecTests/json/README.md3
-rw-r--r--Firestore/Example/Tests/SpecTests/json/collection_spec_test.json147
-rw-r--r--Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json738
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json1150
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limit_spec_test.json1626
-rw-r--r--Firestore/Example/Tests/SpecTests/json/listen_spec_test.json1524
-rw-r--r--Firestore/Example/Tests/SpecTests/json/offline_spec_test.json151
-rw-r--r--Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json155
-rw-r--r--Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json858
-rw-r--r--Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json559
-rw-r--r--Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json250
-rw-r--r--Firestore/Example/Tests/SpecTests/json/write_spec_test.json5437
-rw-r--r--Firestore/Example/Tests/Tests-Info.plist22
-rw-r--r--Firestore/Example/Tests/Util/FSTAssertTests.m105
-rw-r--r--Firestore/Example/Tests/Util/FSTComparisonTests.m143
-rw-r--r--Firestore/Example/Tests/Util/FSTEventAccumulator.h41
-rw-r--r--Firestore/Example/Tests/Util/FSTEventAccumulator.m94
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.h258
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.m348
-rw-r--r--Firestore/Example/Tests/Util/FSTIntegrationTestCase.h94
-rw-r--r--Firestore/Example/Tests/Util/FSTIntegrationTestCase.m285
-rw-r--r--Firestore/Example/Tests/Util/FSTUtilTests.m35
-rw-r--r--Firestore/Example/Tests/Util/XCTestCase+Await.h32
-rw-r--r--Firestore/Example/Tests/Util/XCTestCase+Await.m38
-rw-r--r--Firestore/Example/Tests/en.lproj/InfoPlist.strings2
-rw-r--r--Firestore/Firestore.podspec44
-rw-r--r--Firestore/Port/absl/absl_attributes.h644
-rw-r--r--Firestore/Port/absl/absl_config.h306
-rw-r--r--Firestore/Port/absl/absl_endian.h342
-rw-r--r--Firestore/Port/absl/absl_integral_types.h148
-rw-r--r--Firestore/Port/absl/absl_port.h535
-rw-r--r--Firestore/Port/bits.cc39
-rw-r--r--Firestore/Port/bits.h160
-rw-r--r--Firestore/Port/bits_test.cc138
-rw-r--r--Firestore/Port/ordered_code.cc579
-rw-r--r--Firestore/Port/ordered_code.h116
-rw-r--r--Firestore/Port/ordered_code_test.cc528
-rw-r--r--Firestore/Port/string_util.cc51
-rw-r--r--Firestore/Port/string_util.h66
-rw-r--r--Firestore/Port/string_util_test.cc39
-rw-r--r--Firestore/Protos/FrameworkMaker.xcodeproj/project.pbxproj428
-rw-r--r--Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_iOS.xcscheme91
-rw-r--r--Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_macOS.xcscheme91
-rw-r--r--Firestore/Protos/Podfile11
-rw-r--r--Firestore/Protos/README.md20
-rwxr-xr-xFirestore/Protos/build-protos.sh40
-rw-r--r--Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h132
-rw-r--r--Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.m192
-rw-r--r--Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h138
-rw-r--r--Firestore/Protos/objc/firestore/local/Mutation.pbobjc.m190
-rw-r--r--Firestore/Protos/objc/firestore/local/Target.pbobjc.h208
-rw-r--r--Firestore/Protos/objc/firestore/local/Target.pbobjc.m247
-rw-r--r--Firestore/Protos/objc/google/api/Annotations.pbobjc.h17
-rw-r--r--Firestore/Protos/objc/google/api/Annotations.pbobjc.m17
-rw-r--r--Firestore/Protos/objc/google/api/HTTP.pbobjc.h406
-rw-r--r--Firestore/Protos/objc/google/api/HTTP.pbobjc.m306
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h223
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.m345
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h309
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.m412
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h1342
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.m2064
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h232
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.m281
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h579
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.m907
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h432
-rw-r--r--Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.m653
-rw-r--r--Firestore/Protos/objc/google/rpc/Status.pbobjc.h155
-rw-r--r--Firestore/Protos/objc/google/rpc/Status.pbobjc.m136
-rw-r--r--Firestore/Protos/objc/google/type/Latlng.pbobjc.h127
-rw-r--r--Firestore/Protos/objc/google/type/Latlng.pbobjc.m119
-rw-r--r--Firestore/Protos/protos/firestore/local/maybe_document.proto33
-rw-r--r--Firestore/Protos/protos/firestore/local/mutation.proto44
-rw-r--r--Firestore/Protos/protos/firestore/local/target.proto90
-rw-r--r--Firestore/Protos/protos/google/api/annotations.proto31
-rw-r--r--Firestore/Protos/protos/google/api/http.proto291
-rw-r--r--Firestore/Protos/protos/google/firestore/v1beta1/common.proto82
-rw-r--r--Firestore/Protos/protos/google/firestore/v1beta1/document.proto148
-rw-r--r--Firestore/Protos/protos/google/firestore/v1beta1/firestore.proto719
-rw-r--r--Firestore/Protos/protos/google/firestore/v1beta1/query.proto231
-rw-r--r--Firestore/Protos/protos/google/firestore/v1beta1/write.proto189
-rw-r--r--Firestore/Protos/protos/google/rpc/status.proto92
-rw-r--r--Firestore/Protos/protos/google/type/latlng.proto71
-rwxr-xr-xFirestore/Protos/strip-registry.py36
-rw-r--r--Firestore/README.md15
-rw-r--r--Firestore/Source/API/FIRCollectionReference+Internal.h28
-rw-r--r--Firestore/Source/API/FIRCollectionReference.m113
-rw-r--r--Firestore/Source/API/FIRDocumentChange+Internal.h32
-rw-r--r--Firestore/Source/API/FIRDocumentChange.m129
-rw-r--r--Firestore/Source/API/FIRDocumentReference+Internal.h34
-rw-r--r--Firestore/Source/API/FIRDocumentReference.m285
-rw-r--r--Firestore/Source/API/FIRDocumentSnapshot+Internal.h37
-rw-r--r--Firestore/Source/API/FIRDocumentSnapshot.m175
-rw-r--r--Firestore/Source/API/FIRFieldPath+Internal.h39
-rw-r--r--Firestore/Source/API/FIRFieldPath.m101
-rw-r--r--Firestore/Source/API/FIRFieldValue+Internal.h37
-rw-r--r--Firestore/Source/API/FIRFieldValue.m96
-rw-r--r--Firestore/Source/API/FIRFirestore+Internal.h64
-rw-r--r--Firestore/Source/API/FIRFirestore.m284
-rw-r--r--Firestore/Source/API/FIRFirestoreSettings.m92
-rw-r--r--Firestore/Source/API/FIRFirestoreVersion.h22
-rw-r--r--Firestore/Source/API/FIRFirestoreVersion.m29
-rw-r--r--Firestore/Source/API/FIRGeoPoint+Internal.h26
-rw-r--r--Firestore/Source/API/FIRGeoPoint.m85
-rw-r--r--Firestore/Source/API/FIRListenerRegistration+Internal.h34
-rw-r--r--Firestore/Source/API/FIRListenerRegistration.m57
-rw-r--r--Firestore/Source/API/FIRQuery+Internal.h29
-rw-r--r--Firestore/Source/API/FIRQuery.m520
-rw-r--r--Firestore/Source/API/FIRQuerySnapshot+Internal.h37
-rw-r--r--Firestore/Source/API/FIRQuerySnapshot.m125
-rw-r--r--Firestore/Source/API/FIRQuery_Init.h32
-rw-r--r--Firestore/Source/API/FIRSetOptions+Internal.h33
-rw-r--r--Firestore/Source/API/FIRSetOptions.m65
-rw-r--r--Firestore/Source/API/FIRSnapshotMetadata+Internal.h29
-rw-r--r--Firestore/Source/API/FIRSnapshotMetadata.m49
-rw-r--r--Firestore/Source/API/FIRTransaction+Internal.h27
-rw-r--r--Firestore/Source/API/FIRTransaction.m147
-rw-r--r--Firestore/Source/API/FIRWriteBatch+Internal.h25
-rw-r--r--Firestore/Source/API/FIRWriteBatch.m116
-rw-r--r--Firestore/Source/API/FSTUserDataConverter.h124
-rw-r--r--Firestore/Source/API/FSTUserDataConverter.m568
-rw-r--r--Firestore/Source/Auth/FSTCredentialsProvider.h113
-rw-r--r--Firestore/Source/Auth/FSTCredentialsProvider.m161
-rw-r--r--Firestore/Source/Auth/FSTEmptyCredentialsProvider.h28
-rw-r--r--Firestore/Source/Auth/FSTEmptyCredentialsProvider.m47
-rw-r--r--Firestore/Source/Auth/FSTUser.h43
-rw-r--r--Firestore/Source/Auth/FSTUser.m68
-rw-r--r--Firestore/Source/Core/FSTDatabaseInfo.h55
-rw-r--r--Firestore/Source/Core/FSTDatabaseInfo.m70
-rw-r--r--Firestore/Source/Core/FSTEventManager.h88
-rw-r--r--Firestore/Source/Core/FSTEventManager.m335
-rw-r--r--Firestore/Source/Core/FSTFirestoreClient.h87
-rw-r--r--Firestore/Source/Core/FSTFirestoreClient.m271
-rw-r--r--Firestore/Source/Core/FSTQuery.h269
-rw-r--r--Firestore/Source/Core/FSTQuery.m759
-rw-r--r--Firestore/Source/Core/FSTSnapshotVersion.h43
-rw-r--r--Firestore/Source/Core/FSTSnapshotVersion.m80
-rw-r--r--Firestore/Source/Core/FSTSyncEngine.h105
-rw-r--r--Firestore/Source/Core/FSTSyncEngine.m520
-rw-r--r--Firestore/Source/Core/FSTTargetIDGenerator.h55
-rw-r--r--Firestore/Source/Core/FSTTargetIDGenerator.m105
-rw-r--r--Firestore/Source/Core/FSTTimestamp.h72
-rw-r--r--Firestore/Source/Core/FSTTimestamp.m122
-rw-r--r--Firestore/Source/Core/FSTTransaction.h73
-rw-r--r--Firestore/Source/Core/FSTTransaction.m250
-rw-r--r--Firestore/Source/Core/FSTTypes.h90
-rw-r--r--Firestore/Source/Core/FSTView.h143
-rw-r--r--Firestore/Source/Core/FSTView.m451
-rw-r--r--Firestore/Source/Core/FSTViewSnapshot.h117
-rw-r--r--Firestore/Source/Core/FSTViewSnapshot.m231
-rw-r--r--Firestore/Source/Local/FSTDocumentReference.h61
-rw-r--r--Firestore/Source/Local/FSTDocumentReference.m83
-rw-r--r--Firestore/Source/Local/FSTEagerGarbageCollector.h36
-rw-r--r--Firestore/Source/Local/FSTEagerGarbageCollector.m89
-rw-r--r--Firestore/Source/Local/FSTGarbageCollector.h95
-rw-r--r--Firestore/Source/Local/FSTLevelDB.h105
-rw-r--r--Firestore/Source/Local/FSTLevelDB.mm246
-rw-r--r--Firestore/Source/Local/FSTLevelDBKey.h344
-rw-r--r--Firestore/Source/Local/FSTLevelDBKey.mm757
-rw-r--r--Firestore/Source/Local/FSTLevelDBMutationQueue.h64
-rw-r--r--Firestore/Source/Local/FSTLevelDBMutationQueue.mm637
-rw-r--r--Firestore/Source/Local/FSTLevelDBQueryCache.h54
-rw-r--r--Firestore/Source/Local/FSTLevelDBQueryCache.mm340
-rw-r--r--Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h50
-rw-r--r--Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm153
-rw-r--r--Firestore/Source/Local/FSTLocalDocumentsView.h62
-rw-r--r--Firestore/Source/Local/FSTLocalDocumentsView.m182
-rw-r--r--Firestore/Source/Local/FSTLocalSerializer.h72
-rw-r--r--Firestore/Source/Local/FSTLocalSerializer.m208
-rw-r--r--Firestore/Source/Local/FSTLocalStore.h194
-rw-r--r--Firestore/Source/Local/FSTLocalStore.m546
-rw-r--r--Firestore/Source/Local/FSTLocalViewChanges.h51
-rw-r--r--Firestore/Source/Local/FSTLocalViewChanges.m76
-rw-r--r--Firestore/Source/Local/FSTLocalWriteResult.h36
-rw-r--r--Firestore/Source/Local/FSTLocalWriteResult.m43
-rw-r--r--Firestore/Source/Local/FSTMemoryMutationQueue.h34
-rw-r--r--Firestore/Source/Local/FSTMemoryMutationQueue.m441
-rw-r--r--Firestore/Source/Local/FSTMemoryPersistence.h33
-rw-r--r--Firestore/Source/Local/FSTMemoryPersistence.m107
-rw-r--r--Firestore/Source/Local/FSTMemoryQueryCache.h30
-rw-r--r--Firestore/Source/Local/FSTMemoryQueryCache.m131
-rw-r--r--Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h29
-rw-r--r--Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m84
-rw-r--r--Firestore/Source/Local/FSTMutationQueue.h159
-rw-r--r--Firestore/Source/Local/FSTNoOpGarbageCollector.h32
-rw-r--r--Firestore/Source/Local/FSTNoOpGarbageCollector.m45
-rw-r--r--Firestore/Source/Local/FSTPersistence.h103
-rw-r--r--Firestore/Source/Local/FSTQueryCache.h113
-rw-r--r--Firestore/Source/Local/FSTQueryData.h82
-rw-r--r--Firestore/Source/Local/FSTQueryData.m93
-rw-r--r--Firestore/Source/Local/FSTReferenceSet.h71
-rw-r--r--Firestore/Source/Local/FSTReferenceSet.m135
-rw-r--r--Firestore/Source/Local/FSTRemoteDocumentCache.h76
-rw-r--r--Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h66
-rw-r--r--Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m88
-rw-r--r--Firestore/Source/Local/FSTWriteGroup.h97
-rw-r--r--Firestore/Source/Local/FSTWriteGroup.mm145
-rw-r--r--Firestore/Source/Local/FSTWriteGroupTracker.h45
-rw-r--r--Firestore/Source/Local/FSTWriteGroupTracker.m52
-rw-r--r--Firestore/Source/Local/StringView.h85
-rw-r--r--Firestore/Source/Model/FSTDatabaseID.h48
-rw-r--r--Firestore/Source/Model/FSTDatabaseID.m90
-rw-r--r--Firestore/Source/Model/FSTDocument.h58
-rw-r--r--Firestore/Source/Model/FSTDocument.m139
-rw-r--r--Firestore/Source/Model/FSTDocumentDictionary.h44
-rw-r--r--Firestore/Source/Model/FSTDocumentDictionary.m42
-rw-r--r--Firestore/Source/Model/FSTDocumentKey.h66
-rw-r--r--Firestore/Source/Model/FSTDocumentKey.m105
-rw-r--r--Firestore/Source/Model/FSTDocumentKeySet.h35
-rw-r--r--Firestore/Source/Model/FSTDocumentKeySet.m31
-rw-r--r--Firestore/Source/Model/FSTDocumentSet.h95
-rw-r--r--Firestore/Source/Model/FSTDocumentSet.m197
-rw-r--r--Firestore/Source/Model/FSTDocumentVersionDictionary.h40
-rw-r--r--Firestore/Source/Model/FSTDocumentVersionDictionary.m37
-rw-r--r--Firestore/Source/Model/FSTFieldValue.h242
-rw-r--r--Firestore/Source/Model/FSTFieldValue.m837
-rw-r--r--Firestore/Source/Model/FSTMutation.h325
-rw-r--r--Firestore/Source/Model/FSTMutation.m575
-rw-r--r--Firestore/Source/Model/FSTMutationBatch.h119
-rw-r--r--Firestore/Source/Model/FSTMutationBatch.m176
-rw-r--r--Firestore/Source/Model/FSTPath.h141
-rw-r--r--Firestore/Source/Model/FSTPath.m356
-rw-r--r--Firestore/Source/Public/FIRCollectionReference.h99
-rw-r--r--Firestore/Source/Public/FIRDocumentChange.h70
-rw-r--r--Firestore/Source/Public/FIRDocumentReference.h219
-rw-r--r--Firestore/Source/Public/FIRDocumentSnapshot.h68
-rw-r--r--Firestore/Source/Public/FIRFieldPath.h50
-rw-r--r--Firestore/Source/Public/FIRFieldValue.h45
-rw-r--r--Firestore/Source/Public/FIRFirestore.h145
-rw-r--r--Firestore/Source/Public/FIRFirestoreErrors.h105
-rw-r--r--Firestore/Source/Public/FIRFirestoreSettings.h51
-rw-r--r--Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h29
-rw-r--r--Firestore/Source/Public/FIRGeoPoint.h49
-rw-r--r--Firestore/Source/Public/FIRListenerRegistration.h32
-rw-r--r--Firestore/Source/Public/FIRQuery.h414
-rw-r--r--Firestore/Source/Public/FIRQuerySnapshot.h65
-rw-r--r--Firestore/Source/Public/FIRSetOptions.h46
-rw-r--r--Firestore/Source/Public/FIRSnapshotMetadata.h44
-rw-r--r--Firestore/Source/Public/FIRTransaction.h106
-rw-r--r--Firestore/Source/Public/FIRWriteBatch.h107
-rw-r--r--Firestore/Source/Remote/FSTBufferedWriter.h44
-rw-r--r--Firestore/Source/Remote/FSTBufferedWriter.m134
-rw-r--r--Firestore/Source/Remote/FSTDatastore.h365
-rw-r--r--Firestore/Source/Remote/FSTDatastore.m1027
-rw-r--r--Firestore/Source/Remote/FSTExistenceFilter.h31
-rw-r--r--Firestore/Source/Remote/FSTExistenceFilter.m53
-rw-r--r--Firestore/Source/Remote/FSTExponentialBackoff.h79
-rw-r--r--Firestore/Source/Remote/FSTExponentialBackoff.m97
-rw-r--r--Firestore/Source/Remote/FSTRemoteEvent.h213
-rw-r--r--Firestore/Source/Remote/FSTRemoteEvent.m516
-rw-r--r--Firestore/Source/Remote/FSTRemoteStore.h143
-rw-r--r--Firestore/Source/Remote/FSTRemoteStore.m599
-rw-r--r--Firestore/Source/Remote/FSTSerializerBeta.h110
-rw-r--r--Firestore/Source/Remote/FSTSerializerBeta.m1084
-rw-r--r--Firestore/Source/Remote/FSTWatchChange.h118
-rw-r--r--Firestore/Source/Remote/FSTWatchChange.m150
-rw-r--r--Firestore/Source/Util/FSTAssert.h77
-rw-r--r--Firestore/Source/Util/FSTAsyncQueryListener.h48
-rw-r--r--Firestore/Source/Util/FSTAsyncQueryListener.m50
-rw-r--r--Firestore/Source/Util/FSTClasses.h40
-rw-r--r--Firestore/Source/Util/FSTComparison.h66
-rw-r--r--Firestore/Source/Util/FSTComparison.m175
-rw-r--r--Firestore/Source/Util/FSTDispatchQueue.h58
-rw-r--r--Firestore/Source/Util/FSTDispatchQueue.m75
-rw-r--r--Firestore/Source/Util/FSTLogger.h34
-rw-r--r--Firestore/Source/Util/FSTLogger.m40
-rw-r--r--Firestore/Source/Util/FSTUsageValidation.h45
-rw-r--r--Firestore/Source/Util/FSTUsageValidation.m30
-rw-r--r--Firestore/Source/Util/FSTUtil.h31
-rw-r--r--Firestore/Source/Util/FSTUtil.m44
-rwxr-xr-xFirestore/test.sh49
-rw-r--r--Firestore/third_party/Immutable/FSTArraySortedDictionary.h35
-rw-r--r--Firestore/third_party/Immutable/FSTArraySortedDictionary.m242
-rw-r--r--Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.h26
-rw-r--r--Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.m54
-rw-r--r--Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h120
-rw-r--r--Firestore/third_party/Immutable/FSTImmutableSortedDictionary.m143
-rw-r--r--Firestore/third_party/Immutable/FSTImmutableSortedSet.h47
-rw-r--r--Firestore/third_party/Immutable/FSTImmutableSortedSet.m144
-rw-r--r--Firestore/third_party/Immutable/FSTLLRBEmptyNode.h11
-rw-r--r--Firestore/third_party/Immutable/FSTLLRBEmptyNode.m102
-rw-r--r--Firestore/third_party/Immutable/FSTLLRBNode.h68
-rw-r--r--Firestore/third_party/Immutable/FSTLLRBValueNode.h29
-rw-r--r--Firestore/third_party/Immutable/FSTLLRBValueNode.m308
-rw-r--r--Firestore/third_party/Immutable/FSTTreeSortedDictionary.h41
-rw-r--r--Firestore/third_party/Immutable/FSTTreeSortedDictionary.m382
-rw-r--r--Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.h21
-rw-r--r--Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.m114
-rw-r--r--Firestore/third_party/Immutable/LICENSE21
-rw-r--r--Firestore/third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m467
-rw-r--r--Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h17
-rw-r--r--Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m17
-rw-r--r--Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h20
-rw-r--r--Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m17
-rw-r--r--Firestore/third_party/Immutable/Tests/FSTLLRBValueNode+Test.h10
-rw-r--r--Firestore/third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m655
-rw-r--r--README.md8
-rwxr-xr-xscripts/style.sh1
-rwxr-xr-xtest.sh5
390 files changed, 77624 insertions, 4 deletions
diff --git a/Firebase/Core/FIRLogger.m b/Firebase/Core/FIRLogger.m
index e417879..a607f58 100644
--- a/Firebase/Core/FIRLogger.m
+++ b/Firebase/Core/FIRLogger.m
@@ -32,6 +32,7 @@ FIRLoggerService kFIRLoggerCore = @"[Firebase/Core]";
FIRLoggerService kFIRLoggerCrash = @"[Firebase/Crash]";
FIRLoggerService kFIRLoggerDatabase = @"[Firebase/Database]";
FIRLoggerService kFIRLoggerDynamicLinks = @"[Firebase/DynamicLinks]";
+FIRLoggerService kFIRLoggerFirestore = @"[Firebase/Firestore]";
FIRLoggerService kFIRLoggerInstanceID = @"[Firebase/InstanceID]";
FIRLoggerService kFIRLoggerInvites = @"[Firebase/Invites]";
FIRLoggerService kFIRLoggerMessaging = @"[Firebase/Messaging]";
diff --git a/Firebase/Core/Private/FIRLogger.h b/Firebase/Core/Private/FIRLogger.h
index 260c145..3e62fed 100644
--- a/Firebase/Core/Private/FIRLogger.h
+++ b/Firebase/Core/Private/FIRLogger.h
@@ -33,6 +33,7 @@ extern FIRLoggerService kFIRLoggerCore;
extern FIRLoggerService kFIRLoggerCrash;
extern FIRLoggerService kFIRLoggerDatabase;
extern FIRLoggerService kFIRLoggerDynamicLinks;
+extern FIRLoggerService kFIRLoggerFirestore;
extern FIRLoggerService kFIRLoggerInstanceID;
extern FIRLoggerService kFIRLoggerInvites;
extern FIRLoggerService kFIRLoggerMessaging;
diff --git a/FirebaseCommunity.podspec b/FirebaseCommunity.podspec
index 7a8f2e4..d1c3891 100644
--- a/FirebaseCommunity.podspec
+++ b/FirebaseCommunity.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'FirebaseCommunity'
- s.version = '0.1.2'
+ s.version = '0.1.3'
s.summary = 'Firebase Open Source Libraries for iOS.'
s.description = <<-DESC
diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..979050f
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj
@@ -0,0 +1,1700 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ DE29E7F51F2174B000909613 /* AllTests */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = DE29E7F81F2174B000909613 /* Build configuration list for PBXAggregateTarget "AllTests" */;
+ buildPhases = (
+ );
+ dependencies = (
+ DE0761FA1F2FEE7E003233AF /* PBXTargetDependency */,
+ DE29E7FA1F2174DD00909613 /* PBXTargetDependency */,
+ DE29E7FC1F2174DD00909613 /* PBXTargetDependency */,
+ );
+ name = AllTests;
+ productName = AllTests;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; };
+ 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */; };
+ 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; };
+ 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */; };
+ 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */; };
+ 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */; };
+ 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */; };
+ 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */; };
+ 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */; };
+ 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */; };
+ 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; };
+ 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */; };
+ 54E928221F33952900C1953E /* FSTIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */; };
+ 54E928231F33952D00C1953E /* FSTIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */; };
+ 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; };
+ 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; };
+ 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; };
+ 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; };
+ 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; };
+ 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; };
+ 6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
+ 6003F598195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F596195388D20070C39A /* InfoPlist.strings */; };
+ 6003F59A195388D20070C39A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F599195388D20070C39A /* main.m */; };
+ 6003F59E195388D20070C39A /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F59D195388D20070C39A /* FIRAppDelegate.m */; };
+ 6003F5A7195388D20070C39A /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5A6195388D20070C39A /* FIRViewController.m */; };
+ 6003F5A9195388D20070C39A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5A8195388D20070C39A /* Images.xcassets */; };
+ 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; };
+ 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; };
+ 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
+ 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; };
+ 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */; };
+ 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; };
+ 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; };
+ AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; };
+ C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; };
+ DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */; };
+ DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; };
+ DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
+ DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; };
+ DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; };
+ DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; };
+ DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */; };
+ DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */; };
+ DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */; };
+ DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */; };
+ DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */; };
+ DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */; };
+ DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */; };
+ DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */; };
+ DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */; };
+ DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */; };
+ DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; };
+ DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */ = {isa = PBXBuildFile; fileRef = DE03B3621F215E1600A30B9C /* CAcert.pem */; };
+ DE0761F81F2FE68D003233AF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0761F61F2FE68D003233AF /* main.swift */; };
+ DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */; };
+ DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */; };
+ DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */; };
+ DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */; };
+ DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */; };
+ DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */; };
+ DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */; };
+ DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */; };
+ DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */; };
+ DE51B1D11F0D48CD0013853F /* FSTTargetIDGeneratorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */; };
+ DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */; };
+ DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */; };
+ DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B11F0D48AC0013853F /* FSTViewTests.m */; };
+ DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */; };
+ DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */; };
+ DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */; };
+ DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */; };
+ DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */; };
+ DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */; };
+ DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */; };
+ DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */; };
+ DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */; };
+ DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */; };
+ DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */; };
+ DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */; };
+ DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */; };
+ DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */; };
+ DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */; };
+ DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */; };
+ DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */; };
+ DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */; };
+ DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */; };
+ DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */; };
+ DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */; };
+ DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */; };
+ DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */; };
+ DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */; };
+ DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1811F0D48AC0013853F /* FSTMutationTests.m */; };
+ DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1821F0D48AC0013853F /* FSTPathTests.m */; };
+ DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */; };
+ DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */; };
+ DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */; };
+ DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */; };
+ DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */; };
+ DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */; };
+ DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */; };
+ DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */; };
+ DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */; };
+ DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1991F0D48AC0013853F /* FSTSpecTests.m */; };
+ DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */; };
+ DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1861F0D48AC0013853F /* FSTAssertTests.m */; };
+ DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */; };
+ DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; };
+ DE51B2021F0D493E0013853F /* FSTUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */; };
+ F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 6003F5B3195388D20070C39A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6003F589195388D20070C39A;
+ remoteInfo = Firestore;
+ };
+ DE03B2961F2149D600A30B9C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6003F589195388D20070C39A;
+ remoteInfo = Firestore;
+ };
+ DE0761F91F2FEE7E003233AF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE0761E31F2FE611003233AF;
+ remoteInfo = SwiftBuildTest;
+ };
+ DE29E7F91F2174DD00909613 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6003F5AD195388D20070C39A;
+ remoteInfo = Firestore_Tests;
+ };
+ DE29E7FB1F2174DD00909613 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE03B2941F2149D600A30B9C;
+ remoteInfo = Firestore_IntegrationTests;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.release.xcconfig"; sourceTree = "<group>"; };
+ 12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
+ 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftBuildTest.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = remote_store_spec_test.json; sourceTree = "<group>"; };
+ 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.debug.xcconfig"; sourceTree = "<group>"; };
+ 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.release.xcconfig"; sourceTree = "<group>"; };
+ 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = collection_spec_test.json; sourceTree = "<group>"; };
+ 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = existence_filter_spec_test.json; sourceTree = "<group>"; };
+ 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = limbo_spec_test.json; sourceTree = "<group>"; };
+ 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = limit_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = listen_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = offline_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = orderby_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = persistence_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = resume_token_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A51F315EE100DD57A1 /* write_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = write_spec_test.json; sourceTree = "<group>"; };
+ 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRValidationTests.m; sourceTree = "<group>"; };
+ 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTEventAccumulator.h; sourceTree = "<group>"; };
+ 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTEventAccumulator.m; sourceTree = "<group>"; };
+ 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTIntegrationTestCase.h; sourceTree = "<group>"; };
+ 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTIntegrationTestCase.m; sourceTree = "<group>"; };
+ 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestCase+Await.h"; sourceTree = "<group>"; };
+ 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCTestCase+Await.m"; sourceTree = "<group>"; };
+ 6003F58A195388D20070C39A /* Firestore_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firestore_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+ 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
+ 6003F591195388D20070C39A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
+ 6003F595195388D20070C39A /* Firestore-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Firestore-Info.plist"; sourceTree = "<group>"; };
+ 6003F597195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ 6003F599195388D20070C39A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ 6003F59C195388D20070C39A /* FIRAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRAppDelegate.h; sourceTree = "<group>"; };
+ 6003F59D195388D20070C39A /* FIRAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRAppDelegate.m; sourceTree = "<group>"; };
+ 6003F5A5195388D20070C39A /* FIRViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRViewController.h; sourceTree = "<group>"; };
+ 6003F5A6195388D20070C39A /* FIRViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRViewController.m; sourceTree = "<group>"; };
+ 6003F5A8195388D20070C39A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
+ 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 6003F5AF195388D20070C39A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
+ 6003F5B7195388D20070C39A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = "<group>"; };
+ 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ 8E002F4AD5D9B6197C940847 /* Firestore.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = Firestore.podspec; path = ../Firestore.podspec; sourceTree = "<group>"; };
+ 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.debug.xcconfig"; sourceTree = "<group>"; };
+ 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.debug.xcconfig"; sourceTree = "<group>"; };
+ B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.debug.xcconfig"; sourceTree = "<group>"; };
+ D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
+ DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.release.xcconfig"; sourceTree = "<group>"; };
+ DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE03B3621F215E1600A30B9C /* CAcert.pem */ = {isa = PBXFileReference; lastKnownFileType = text; path = CAcert.pem; sourceTree = "<group>"; };
+ DE0761E41F2FE611003233AF /* SwiftBuildTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftBuildTest.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE0761F61F2FE68D003233AF /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
+ DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTArraySortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m; sourceTree = "<group>"; };
+ DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedDictionary+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h"; sourceTree = "<group>"; };
+ DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedDictionary+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m"; sourceTree = "<group>"; };
+ DE2EF0811F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedSet+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h"; sourceTree = "<group>"; };
+ DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedSet+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m"; sourceTree = "<group>"; };
+ DE2EF0831F3D0B6E003D0CDC /* FSTLLRBValueNode+Test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTLLRBValueNode+Test.h"; path = "../../third_party/Immutable/Tests/FSTLLRBValueNode+Test.h"; sourceTree = "<group>"; };
+ DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTTreeSortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m; sourceTree = "<group>"; };
+ DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEagerGarbageCollectorTests.m; sourceTree = "<group>"; };
+ DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBKeyTests.mm; sourceTree = "<group>"; };
+ DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBLocalStoreTests.m; sourceTree = "<group>"; };
+ DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBMutationQueueTests.mm; sourceTree = "<group>"; };
+ DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBQueryCacheTests.m; sourceTree = "<group>"; };
+ DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBRemoteDocumentCacheTests.mm; sourceTree = "<group>"; };
+ DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalSerializerTests.m; sourceTree = "<group>"; };
+ DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTLocalStoreTests.h; sourceTree = "<group>"; };
+ DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalStoreTests.m; sourceTree = "<group>"; };
+ DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryLocalStoreTests.m; sourceTree = "<group>"; };
+ DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryMutationQueueTests.m; sourceTree = "<group>"; };
+ DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryQueryCacheTests.m; sourceTree = "<group>"; };
+ DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryRemoteDocumentCacheTests.m; sourceTree = "<group>"; };
+ DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMutationQueueTests.h; sourceTree = "<group>"; };
+ DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationQueueTests.m; sourceTree = "<group>"; };
+ DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTPersistenceTestHelpers.h; sourceTree = "<group>"; };
+ DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPersistenceTestHelpers.m; sourceTree = "<group>"; };
+ DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTQueryCacheTests.h; sourceTree = "<group>"; };
+ DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryCacheTests.m; sourceTree = "<group>"; };
+ DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTReferenceSetTests.m; sourceTree = "<group>"; };
+ DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTRemoteDocumentCacheTests.h; sourceTree = "<group>"; };
+ DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentCacheTests.m; sourceTree = "<group>"; };
+ DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentChangeBufferTests.m; sourceTree = "<group>"; };
+ DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTWriteGroupTests.mm; sourceTree = "<group>"; };
+ DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseIDTests.m; sourceTree = "<group>"; };
+ DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentKeyTests.m; sourceTree = "<group>"; };
+ DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentSetTests.m; sourceTree = "<group>"; };
+ DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentTests.m; sourceTree = "<group>"; };
+ DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTFieldValueTests.m; sourceTree = "<group>"; };
+ DE51B1811F0D48AC0013853F /* FSTMutationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationTests.m; sourceTree = "<group>"; };
+ DE51B1821F0D48AC0013853F /* FSTPathTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPathTests.m; sourceTree = "<group>"; };
+ DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRGeoPointTests.m; sourceTree = "<group>"; };
+ DE51B1861F0D48AC0013853F /* FSTAssertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTAssertTests.m; sourceTree = "<group>"; };
+ DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTComparisonTests.m; sourceTree = "<group>"; };
+ DE51B1881F0D48AC0013853F /* FSTHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTHelpers.h; sourceTree = "<group>"; };
+ DE51B1891F0D48AC0013853F /* FSTHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTHelpers.m; sourceTree = "<group>"; };
+ DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTUtilTests.m; sourceTree = "<group>"; };
+ DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBSpecTests.m; sourceTree = "<group>"; };
+ DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemorySpecTests.m; sourceTree = "<group>"; };
+ DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMockDatastore.h; sourceTree = "<group>"; };
+ DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMockDatastore.m; sourceTree = "<group>"; };
+ DE51B1981F0D48AC0013853F /* FSTSpecTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSpecTests.h; sourceTree = "<group>"; };
+ DE51B1991F0D48AC0013853F /* FSTSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSpecTests.m; sourceTree = "<group>"; };
+ DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSyncEngineTestDriver.h; sourceTree = "<group>"; };
+ DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSyncEngineTestDriver.m; sourceTree = "<group>"; };
+ DE51B1A71F0D48AC0013853F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
+ DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseInfoTests.m; sourceTree = "<group>"; };
+ DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEventManagerTests.m; sourceTree = "<group>"; };
+ DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryListenerTests.m; sourceTree = "<group>"; };
+ DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryTests.m; sourceTree = "<group>"; };
+ DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTSyncEngine+Testing.h"; sourceTree = "<group>"; };
+ DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTargetIDGeneratorTests.m; sourceTree = "<group>"; };
+ DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTimestampTests.m; sourceTree = "<group>"; };
+ DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewSnapshotTest.m; sourceTree = "<group>"; };
+ DE51B1B11F0D48AC0013853F /* FSTViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewTests.m; sourceTree = "<group>"; };
+ DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = "<group>"; };
+ DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteEventTests.m; sourceTree = "<group>"; };
+ DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSerializerBetaTests.m; sourceTree = "<group>"; };
+ DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTStreamTests.m; sourceTree = "<group>"; };
+ DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTWatchChange+Testing.h"; sourceTree = "<group>"; };
+ DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FSTWatchChange+Testing.m"; sourceTree = "<group>"; };
+ DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTWatchChangeTests.m; sourceTree = "<group>"; };
+ DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRCursorTests.m; sourceTree = "<group>"; };
+ DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRDatabaseTests.m; sourceTree = "<group>"; };
+ DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRFieldsTests.m; sourceTree = "<group>"; };
+ DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRListenerRegistrationTests.m; sourceTree = "<group>"; };
+ DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRQueryTests.m; sourceTree = "<group>"; };
+ DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRServerTimestampTests.m; sourceTree = "<group>"; };
+ DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRTypeTests.m; sourceTree = "<group>"; };
+ DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = "<group>"; };
+ DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSmokeTests.m; sourceTree = "<group>"; };
+ DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTransactionTests.m; sourceTree = "<group>"; };
+ DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRWriteBatchTests.m; sourceTree = "<group>"; };
+ F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.release.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 6003F587195388D20070C39A /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */,
+ 6003F592195388D20070C39A /* UIKit.framework in Frameworks */,
+ 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */,
+ 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6003F5AB195388D20070C39A /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */,
+ 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */,
+ 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */,
+ F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE03B2D31F2149D600A30B9C /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */,
+ DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */,
+ DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */,
+ DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */,
+ AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE0761E11F2FE611003233AF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 6003F581195388D10070C39A = {
+ isa = PBXGroup;
+ children = (
+ 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */,
+ 6003F593195388D20070C39A /* Example for Firestore */,
+ 6003F5B5195388D20070C39A /* Tests */,
+ DE0761E51F2FE611003233AF /* SwiftBuildTest */,
+ 6003F58C195388D20070C39A /* Frameworks */,
+ 6003F58B195388D20070C39A /* Products */,
+ A47A1BF74A48BCAEAFBCBF1E /* Pods */,
+ );
+ sourceTree = "<group>";
+ };
+ 6003F58B195388D20070C39A /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F58A195388D20070C39A /* Firestore_Example.app */,
+ 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */,
+ DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */,
+ DE0761E41F2FE611003233AF /* SwiftBuildTest.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 6003F58C195388D20070C39A /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F58D195388D20070C39A /* Foundation.framework */,
+ 6003F58F195388D20070C39A /* CoreGraphics.framework */,
+ 6003F591195388D20070C39A /* UIKit.framework */,
+ 6003F5AF195388D20070C39A /* XCTest.framework */,
+ 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */,
+ 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */,
+ B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */,
+ 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ 6003F593195388D20070C39A /* Example for Firestore */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F59C195388D20070C39A /* FIRAppDelegate.h */,
+ 6003F59D195388D20070C39A /* FIRAppDelegate.m */,
+ 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */,
+ 6003F5A5195388D20070C39A /* FIRViewController.h */,
+ 6003F5A6195388D20070C39A /* FIRViewController.m */,
+ 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */,
+ 6003F5A8195388D20070C39A /* Images.xcassets */,
+ 6003F594195388D20070C39A /* Supporting Files */,
+ );
+ name = "Example for Firestore";
+ path = Firestore;
+ sourceTree = "<group>";
+ };
+ 6003F594195388D20070C39A /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F595195388D20070C39A /* Firestore-Info.plist */,
+ 6003F596195388D20070C39A /* InfoPlist.strings */,
+ 6003F599195388D20070C39A /* main.m */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ 6003F5B5195388D20070C39A /* Tests */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1831F0D48AC0013853F /* API */,
+ DE51B1A81F0D48AC0013853F /* Core */,
+ DE2EF06E1F3D07D7003D0CDC /* Immutable */,
+ DE51B1BB1F0D48AC0013853F /* Integration */,
+ DE51B1621F0D48AC0013853F /* Local */,
+ DE51B17B1F0D48AC0013853F /* Model */,
+ DE51B1B21F0D48AC0013853F /* Remote */,
+ DE51B1931F0D48AC0013853F /* SpecTests */,
+ DE51B1851F0D48AC0013853F /* Util */,
+ 6003F5B6195388D20070C39A /* Supporting Files */,
+ );
+ path = Tests;
+ sourceTree = "<group>";
+ };
+ 6003F5B6195388D20070C39A /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F5B7195388D20070C39A /* Tests-Info.plist */,
+ 6003F5B8195388D20070C39A /* InfoPlist.strings */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */ = {
+ isa = PBXGroup;
+ children = (
+ 8E002F4AD5D9B6197C940847 /* Firestore.podspec */,
+ D3CC3DC5338DCAF43A211155 /* README.md */,
+ 12F4357299652983A615F886 /* LICENSE */,
+ );
+ name = "Podspec Metadata";
+ sourceTree = "<group>";
+ };
+ A47A1BF74A48BCAEAFBCBF1E /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */,
+ 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */,
+ 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */,
+ DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */,
+ CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */,
+ 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */,
+ 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */,
+ F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "<group>";
+ };
+ DE0761E51F2FE611003233AF /* SwiftBuildTest */ = {
+ isa = PBXGroup;
+ children = (
+ DE0761F61F2FE68D003233AF /* main.swift */,
+ );
+ path = SwiftBuildTest;
+ sourceTree = "<group>";
+ };
+ DE2EF06E1F3D07D7003D0CDC /* Immutable */ = {
+ isa = PBXGroup;
+ children = (
+ DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */,
+ DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */,
+ DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */,
+ DE2EF0811F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.h */,
+ DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */,
+ DE2EF0831F3D0B6E003D0CDC /* FSTLLRBValueNode+Test.h */,
+ DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */,
+ );
+ name = Immutable;
+ sourceTree = "<group>";
+ };
+ DE51B1621F0D48AC0013853F /* Local */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */,
+ DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */,
+ DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */,
+ DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */,
+ DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */,
+ DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */,
+ DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */,
+ DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */,
+ DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */,
+ DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */,
+ DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */,
+ DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */,
+ DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */,
+ DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */,
+ DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */,
+ DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */,
+ DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */,
+ DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */,
+ DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */,
+ DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */,
+ DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */,
+ DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */,
+ DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */,
+ DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */,
+ );
+ path = Local;
+ sourceTree = "<group>";
+ };
+ DE51B17B1F0D48AC0013853F /* Model */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */,
+ DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */,
+ DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */,
+ DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */,
+ DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */,
+ DE51B1811F0D48AC0013853F /* FSTMutationTests.m */,
+ DE51B1821F0D48AC0013853F /* FSTPathTests.m */,
+ );
+ path = Model;
+ sourceTree = "<group>";
+ };
+ DE51B1831F0D48AC0013853F /* API */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */,
+ );
+ path = API;
+ sourceTree = "<group>";
+ };
+ DE51B1851F0D48AC0013853F /* Util */ = {
+ isa = PBXGroup;
+ children = (
+ 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */,
+ 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */,
+ 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */,
+ 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */,
+ DE51B1861F0D48AC0013853F /* FSTAssertTests.m */,
+ DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */,
+ DE51B1881F0D48AC0013853F /* FSTHelpers.h */,
+ DE51B1891F0D48AC0013853F /* FSTHelpers.m */,
+ DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */,
+ 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */,
+ 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */,
+ );
+ path = Util;
+ sourceTree = "<group>";
+ };
+ DE51B1931F0D48AC0013853F /* SpecTests */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */,
+ DE51B1981F0D48AC0013853F /* FSTSpecTests.h */,
+ DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */,
+ DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */,
+ DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */,
+ DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */,
+ DE51B1991F0D48AC0013853F /* FSTSpecTests.m */,
+ DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */,
+ DE51B19C1F0D48AC0013853F /* json */,
+ );
+ path = SpecTests;
+ sourceTree = "<group>";
+ };
+ DE51B19C1F0D48AC0013853F /* json */ = {
+ isa = PBXGroup;
+ children = (
+ 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */,
+ 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */,
+ 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */,
+ 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */,
+ 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */,
+ 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */,
+ 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */,
+ 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */,
+ 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */,
+ 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */,
+ 54DA12A51F315EE100DD57A1 /* write_spec_test.json */,
+ DE51B1A71F0D48AC0013853F /* README.md */,
+ );
+ path = json;
+ sourceTree = "<group>";
+ };
+ DE51B1A81F0D48AC0013853F /* Core */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */,
+ DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */,
+ DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */,
+ DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */,
+ DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */,
+ DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */,
+ DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */,
+ DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */,
+ DE51B1B11F0D48AC0013853F /* FSTViewTests.m */,
+ );
+ path = Core;
+ sourceTree = "<group>";
+ };
+ DE51B1B21F0D48AC0013853F /* Remote */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */,
+ DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */,
+ DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */,
+ DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */,
+ DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */,
+ DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */,
+ DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */,
+ );
+ path = Remote;
+ sourceTree = "<group>";
+ };
+ DE51B1BB1F0D48AC0013853F /* Integration */ = {
+ isa = PBXGroup;
+ children = (
+ DE03B3621F215E1600A30B9C /* CAcert.pem */,
+ DE51B1BC1F0D48AC0013853F /* API */,
+ DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */,
+ DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */,
+ DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */,
+ DE51B1C71F0D48AC0013853F /* Util */,
+ );
+ path = Integration;
+ sourceTree = "<group>";
+ };
+ DE51B1BC1F0D48AC0013853F /* API */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */,
+ DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */,
+ DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */,
+ DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */,
+ DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */,
+ DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */,
+ DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */,
+ 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */,
+ DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */,
+ );
+ path = API;
+ sourceTree = "<group>";
+ };
+ DE51B1C71F0D48AC0013853F /* Util */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = Util;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 6003F589195388D20070C39A /* Firestore_Example */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Example" */;
+ buildPhases = (
+ FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */,
+ 6003F586195388D20070C39A /* Sources */,
+ 6003F587195388D20070C39A /* Frameworks */,
+ 6003F588195388D20070C39A /* Resources */,
+ 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */,
+ DEB4B96019F51073F0553ABC /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Firestore_Example;
+ productName = Firestore;
+ productReference = 6003F58A195388D20070C39A /* Firestore_Example.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 6003F5AD195388D20070C39A /* Firestore_Tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Tests" */;
+ buildPhases = (
+ 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */,
+ 6003F5AA195388D20070C39A /* Sources */,
+ 6003F5AB195388D20070C39A /* Frameworks */,
+ 6003F5AC195388D20070C39A /* Resources */,
+ BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */,
+ AB3F19DA92555D3399DB07CE /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 6003F5B4195388D20070C39A /* PBXTargetDependency */,
+ );
+ name = Firestore_Tests;
+ productName = FirestoreTests;
+ productReference = 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests" */;
+ buildPhases = (
+ DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */,
+ DE03B2981F2149D600A30B9C /* Sources */,
+ DE03B2D31F2149D600A30B9C /* Frameworks */,
+ DE03B2D81F2149D600A30B9C /* Resources */,
+ DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */,
+ DE03B2E51F2149D600A30B9C /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ DE03B2951F2149D600A30B9C /* PBXTargetDependency */,
+ );
+ name = Firestore_IntegrationTests;
+ productName = FirestoreTests;
+ productReference = DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DE0761E31F2FE611003233AF /* SwiftBuildTest */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DE0761F51F2FE611003233AF /* Build configuration list for PBXNativeTarget "SwiftBuildTest" */;
+ buildPhases = (
+ 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */,
+ DE0761E01F2FE611003233AF /* Sources */,
+ DE0761E11F2FE611003233AF /* Frameworks */,
+ DE0761E21F2FE611003233AF /* Resources */,
+ 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */,
+ 04C27A4B1FAE812E8153B724 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = SwiftBuildTest;
+ productName = SwiftBuildTest;
+ productReference = DE0761E41F2FE611003233AF /* SwiftBuildTest.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 6003F582195388D10070C39A /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ CLASSPREFIX = FIR;
+ LastSwiftUpdateCheck = 0830;
+ LastUpgradeCheck = 0720;
+ ORGANIZATIONNAME = Google;
+ TargetAttributes = {
+ 6003F5AD195388D20070C39A = {
+ DevelopmentTeam = EQHXZ8M8AV;
+ TestTargetID = 6003F589195388D20070C39A;
+ };
+ DE03B2941F2149D600A30B9C = {
+ DevelopmentTeam = EQHXZ8M8AV;
+ };
+ DE0761E31F2FE611003233AF = {
+ CreatedOnToolsVersion = 8.3.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ };
+ DE29E7F51F2174B000909613 = {
+ CreatedOnToolsVersion = 9.0;
+ DevelopmentTeam = EQHXZ8M8AV;
+ };
+ };
+ };
+ buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firestore" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 6003F581195388D10070C39A;
+ productRefGroup = 6003F58B195388D20070C39A /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 6003F589195388D20070C39A /* Firestore_Example */,
+ 6003F5AD195388D20070C39A /* Firestore_Tests */,
+ DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */,
+ DE29E7F51F2174B000909613 /* AllTests */,
+ DE0761E31F2FE611003233AF /* SwiftBuildTest */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 6003F588195388D20070C39A /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */,
+ 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */,
+ 6003F5A9195388D20070C39A /* Images.xcassets in Resources */,
+ 6003F598195388D20070C39A /* InfoPlist.strings in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6003F5AC195388D20070C39A /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */,
+ 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */,
+ 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */,
+ 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */,
+ 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */,
+ 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */,
+ 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */,
+ 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */,
+ 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */,
+ 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */,
+ 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */,
+ 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE03B2D81F2149D600A30B9C /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */,
+ DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE0761E21F2FE611003233AF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 04C27A4B1FAE812E8153B724 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseCommunity/FirebaseCommunity.framework",
+ "${BUILT_PRODUCTS_DIR}/Firestore/Firestore.framework",
+ "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
+ "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework",
+ "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework",
+ "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
+ "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCommunity.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Firestore.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseCommunity/FirebaseCommunity.framework",
+ "${BUILT_PRODUCTS_DIR}/Firestore/Firestore.framework",
+ "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
+ "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework",
+ "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework",
+ "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
+ "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCommunity.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Firestore.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Firestore_Tests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-SwiftBuildTest-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ AB3F19DA92555D3399DB07CE /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
+ "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Firestore_IntegrationTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ DE03B2E51F2149D600A30B9C /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ DEB4B96019F51073F0553ABC /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Firestore_Example-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 6003F586195388D20070C39A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6003F59E195388D20070C39A /* FIRAppDelegate.m in Sources */,
+ 6003F5A7195388D20070C39A /* FIRViewController.m in Sources */,
+ 6003F59A195388D20070C39A /* main.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6003F5AA195388D20070C39A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */,
+ DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */,
+ DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */,
+ DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */,
+ DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */,
+ DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */,
+ DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */,
+ DE51B2021F0D493E0013853F /* FSTUtilTests.m in Sources */,
+ DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */,
+ DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */,
+ DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */,
+ DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */,
+ DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */,
+ DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */,
+ DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */,
+ DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */,
+ DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */,
+ 54E928221F33952900C1953E /* FSTIntegrationTestCase.m in Sources */,
+ DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */,
+ DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */,
+ DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */,
+ 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */,
+ DE51B1D11F0D48CD0013853F /* FSTTargetIDGeneratorTests.m in Sources */,
+ DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */,
+ DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */,
+ DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */,
+ DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */,
+ DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */,
+ DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */,
+ DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */,
+ DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */,
+ DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */,
+ DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */,
+ DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */,
+ 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */,
+ DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */,
+ DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */,
+ DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */,
+ DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */,
+ DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */,
+ DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */,
+ DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */,
+ DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */,
+ DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */,
+ DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */,
+ DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */,
+ DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */,
+ DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */,
+ DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */,
+ DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */,
+ DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */,
+ DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */,
+ DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */,
+ DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */,
+ DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */,
+ DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */,
+ DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE03B2981F2149D600A30B9C /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */,
+ DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */,
+ 54E928231F33952D00C1953E /* FSTIntegrationTestCase.m in Sources */,
+ DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */,
+ DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */,
+ 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */,
+ DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */,
+ 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */,
+ DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */,
+ DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */,
+ DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */,
+ DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */,
+ DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */,
+ DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */,
+ DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */,
+ 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE0761E01F2FE611003233AF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE0761F81F2FE68D003233AF /* main.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 6003F5B4195388D20070C39A /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6003F589195388D20070C39A /* Firestore_Example */;
+ targetProxy = 6003F5B3195388D20070C39A /* PBXContainerItemProxy */;
+ };
+ DE03B2951F2149D600A30B9C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6003F589195388D20070C39A /* Firestore_Example */;
+ targetProxy = DE03B2961F2149D600A30B9C /* PBXContainerItemProxy */;
+ };
+ DE0761FA1F2FEE7E003233AF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE0761E31F2FE611003233AF /* SwiftBuildTest */;
+ targetProxy = DE0761F91F2FEE7E003233AF /* PBXContainerItemProxy */;
+ };
+ DE29E7FA1F2174DD00909613 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6003F5AD195388D20070C39A /* Firestore_Tests */;
+ targetProxy = DE29E7F91F2174DD00909613 /* PBXContainerItemProxy */;
+ };
+ DE29E7FC1F2174DD00909613 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */;
+ targetProxy = DE29E7FB1F2174DD00909613 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 6003F596195388D20070C39A /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 6003F597195388D20070C39A /* en */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+ 6003F5B8195388D20070C39A /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 6003F5B9195388D20070C39A /* en */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+ 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 71719F9E1E33DC2100824A3D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 6003F5BD195388D20070C39A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ ENABLE_TESTABILITY = YES;
+ 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_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 6003F5BE195388D20070C39A /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = YES;
+ ENABLE_NS_ASSERTIONS = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 6003F5C0195388D20070C39A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"",
+ "\"${PODS_ROOT}/leveldb-library/\"",
+ "\"${PODS_ROOT}/leveldb-library/include\"",
+ );
+ INFOPLIST_FILE = "Firestore/Firestore-Info.plist";
+ MODULE_NAME = ExampleApp;
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Debug;
+ };
+ 6003F5C1195388D20070C39A /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"",
+ "\"${PODS_ROOT}/leveldb-library/\"",
+ "\"${PODS_ROOT}/leveldb-library/include\"",
+ );
+ INFOPLIST_FILE = "Firestore/Firestore-Info.plist";
+ MODULE_NAME = ExampleApp;
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Release;
+ };
+ 6003F5C3195388D20070C39A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "\"$(inherited)\"",
+ "\"${PODS_ROOT}/../../Source\"",
+ "\"${PODS_ROOT}/../../Source/API\"",
+ "\"${PODS_ROOT}/../../Source/Core\"",
+ "\"${PODS_ROOT}/../../Source/Remote\"",
+ "\"${PODS_ROOT}/../../Source/Model\"",
+ "\"${PODS_ROOT}/../../third_party\"",
+ "\"${PODS_ROOT}/../../third_party/Immutable\"",
+ "\"${PODS_ROOT}/../../\"",
+ "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/api\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/type\"",
+ );
+ INFOPLIST_FILE = "Tests/Tests-Info.plist";
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Debug;
+ };
+ 6003F5C4195388D20070C39A /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "\"$(inherited)\"",
+ "\"${PODS_ROOT}/../../Source\"",
+ "\"${PODS_ROOT}/../../Source/API\"",
+ "\"${PODS_ROOT}/../../Source/Core\"",
+ "\"${PODS_ROOT}/../../Source/Remote\"",
+ "\"${PODS_ROOT}/../../Source/Model\"",
+ "\"${PODS_ROOT}/../../third_party\"",
+ "\"${PODS_ROOT}/../../third_party/Immutable\"",
+ "\"${PODS_ROOT}/../../Protos/objc/firebase/datastore/clients/proto\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/api\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/type\"",
+ "\"${PODS_ROOT}/../../\"",
+ );
+ INFOPLIST_FILE = "Tests/Tests-Info.plist";
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Release;
+ };
+ DE03B2E71F2149D600A30B9C /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "\"$(inherited)\"",
+ "\"${PODS_ROOT}/../../Source\"",
+ "\"${PODS_ROOT}/../../Source/API\"",
+ "\"${PODS_ROOT}/../../Source/Core\"",
+ "\"${PODS_ROOT}/../../Source/Remote\"",
+ "\"${PODS_ROOT}/../../Source/Model\"",
+ "\"${PODS_ROOT}/../../third_party\"",
+ "\"${PODS_ROOT}/../../third_party/Immutable\"",
+ "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/api\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/type\"",
+ "\"${PODS_ROOT}/../../\"",
+ );
+ INFOPLIST_FILE = "Tests/Tests-Info.plist";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-l\"c++\"",
+ "-framework",
+ "\"OCMock\"",
+ "-framework",
+ "\"leveldb\"",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Debug;
+ };
+ DE03B2E81F2149D600A30B9C /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "\"$(inherited)\"",
+ "\"${PODS_ROOT}/../../Source\"",
+ "\"${PODS_ROOT}/../../Source/API\"",
+ "\"${PODS_ROOT}/../../Source/Core\"",
+ "\"${PODS_ROOT}/../../Source/Remote\"",
+ "\"${PODS_ROOT}/../../Source/Model\"",
+ "\"${PODS_ROOT}/../../third_party\"",
+ "\"${PODS_ROOT}/../../third_party/Immutable\"",
+ "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/api\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/type\"",
+ "\"${PODS_ROOT}/../../\"",
+ );
+ INFOPLIST_FILE = "Tests/Tests-Info.plist";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-l\"c++\"",
+ "-framework",
+ "\"OCMock\"",
+ "-framework",
+ "\"leveldb\"",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Release;
+ };
+ DE0761F31F2FE611003233AF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_NO_COMMON_BLOCKS = YES;
+ INFOPLIST_FILE = "Firestore/Firestore-Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 10.3;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBuildTest;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Debug;
+ };
+ DE0761F41F2FE611003233AF /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_NO_COMMON_BLOCKS = YES;
+ INFOPLIST_FILE = "Firestore/Firestore-Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 10.3;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBuildTest;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Release;
+ };
+ DE29E7F61F2174B000909613 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ DE29E7F71F2174B000909613 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firestore" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6003F5BD195388D20070C39A /* Debug */,
+ 6003F5BE195388D20070C39A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Example" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6003F5C0195388D20070C39A /* Debug */,
+ 6003F5C1195388D20070C39A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6003F5C3195388D20070C39A /* Debug */,
+ 6003F5C4195388D20070C39A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE03B2E71F2149D600A30B9C /* Debug */,
+ DE03B2E81F2149D600A30B9C /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE0761F51F2FE611003233AF /* Build configuration list for PBXNativeTarget "SwiftBuildTest" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE0761F31F2FE611003233AF /* Debug */,
+ DE0761F41F2FE611003233AF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE29E7F81F2174B000909613 /* Build configuration list for PBXAggregateTarget "AllTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE29E7F61F2174B000909613 /* Debug */,
+ DE29E7F71F2174B000909613 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 6003F582195388D10070C39A /* Project object */;
+}
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme
new file mode 100644
index 0000000..aacb70e
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0900"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE29E7F51F2174B000909613"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE03B2941F2149D600A30B9C"
+ BuildableName = "Firestore_IntegrationTests.xctest"
+ BlueprintName = "Firestore_IntegrationTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F5AD195388D20070C39A"
+ BuildableName = "Firestore_Tests.xctest"
+ BlueprintName = "Firestore_Tests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE29E7F51F2174B000909613"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE29E7F51F2174B000909613"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE29E7F51F2174B000909613"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme
new file mode 100644
index 0000000..a8538f5
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0720"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F589195388D20070C39A"
+ BuildableName = "Firestore_Example.app"
+ BlueprintName = "Firestore_Example"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F5AD195388D20070C39A"
+ BuildableName = "Firestore_Tests.xctest"
+ BlueprintName = "Firestore_Tests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE03B2941F2149D600A30B9C"
+ BuildableName = "Firestore_IntegrationTests.xctest"
+ BlueprintName = "Firestore_IntegrationTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F589195388D20070C39A"
+ BuildableName = "Firestore_Example.app"
+ BlueprintName = "Firestore_Example"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F589195388D20070C39A"
+ BuildableName = "Firestore_Example.app"
+ BlueprintName = "Firestore_Example"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F589195388D20070C39A"
+ BuildableName = "Firestore_Example.app"
+ BlueprintName = "Firestore_Example"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme
new file mode 100644
index 0000000..ccd099f
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0900"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForRunning = "YES"
+ buildForTesting = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE03B2941F2149D600A30B9C"
+ BuildableName = "Firestore_IntegrationTests.xctest"
+ BlueprintName = "Firestore_IntegrationTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE03B2941F2149D600A30B9C"
+ BuildableName = "Firestore_IntegrationTests.xctest"
+ BlueprintName = "Firestore_IntegrationTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme
new file mode 100644
index 0000000..920e1f3
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0900"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForRunning = "YES"
+ buildForTesting = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F5AD195388D20070C39A"
+ BuildableName = "Firestore_Tests.xctest"
+ BlueprintName = "Firestore_Tests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F5AD195388D20070C39A"
+ BuildableName = "Firestore_Tests.xctest"
+ BlueprintName = "Firestore_Tests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme
new file mode 100644
index 0000000..112a0d0
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE0761E31F2FE611003233AF"
+ BuildableName = "SwiftBuildTest.app"
+ BlueprintName = "SwiftBuildTest"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE0761E31F2FE611003233AF"
+ BuildableName = "SwiftBuildTest.app"
+ BlueprintName = "SwiftBuildTest"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE0761E31F2FE611003233AF"
+ BuildableName = "SwiftBuildTest.app"
+ BlueprintName = "SwiftBuildTest"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE0761E31F2FE611003233AF"
+ BuildableName = "SwiftBuildTest.app"
+ BlueprintName = "SwiftBuildTest"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard b/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..66a7681
--- /dev/null
+++ b/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Firestore/Example/Firestore/Base.lproj/Main.storyboard b/Firestore/Example/Firestore/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..d164a23
--- /dev/null
+++ b/Firestore/Example/Firestore/Base.lproj/Main.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="whP-gf-Uak">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="wQg-tq-qST">
+ <objects>
+ <viewController id="whP-gf-Uak" customClass="FIRViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="uEw-UM-LJ8"/>
+ <viewControllerLayoutGuide type="bottom" id="Mvr-aV-6Um"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="TpU-gO-2f1">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="tc2-Qw-aMS" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="305" y="433"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Firestore/Example/Firestore/FIRAppDelegate.h b/Firestore/Example/Firestore/FIRAppDelegate.h
new file mode 100644
index 0000000..1eb5040
--- /dev/null
+++ b/Firestore/Example/Firestore/FIRAppDelegate.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 UIKit;
+
+@interface FIRAppDelegate : UIResponder <UIApplicationDelegate>
+
+@property(strong, nonatomic) UIWindow *window;
+
+@end
diff --git a/Firestore/Example/Firestore/FIRAppDelegate.m b/Firestore/Example/Firestore/FIRAppDelegate.m
new file mode 100644
index 0000000..12ca249
--- /dev/null
+++ b/Firestore/Example/Firestore/FIRAppDelegate.m
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRAppDelegate.h"
+
+@implementation FIRAppDelegate
+
+- (BOOL)application:(UIApplication *)application
+ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ // Override point for customization after application launch.
+ return YES;
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application {
+ // Sent when the application is about to move from active to inactive state. This can occur for
+ // certain types of temporary interruptions (such as an incoming phone call or SMS message) or
+ // when the user quits the application and it begins the transition to the background state. Use
+ // this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates.
+ // Games should use this method to pause the game.
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application {
+ // Use this method to release shared resources, save user data, invalidate timers, and store
+ // enough application state information to restore your application to its current state in case
+ // it is terminated later. If your application supports background execution, this method is
+ // called instead of applicationWillTerminate: when the user quits.
+}
+
+- (void)applicationWillEnterForeground:(UIApplication *)application {
+ // Called as part of the transition from the background to the inactive state; here you can undo
+ // many of the changes made on entering the background.
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)application {
+ // Restart any tasks that were paused (or not yet started) while the application was inactive. If
+ // the application was previously in the background, optionally refresh the user interface.
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application {
+ // Called when the application is about to terminate. Save data if appropriate. See also
+ // applicationDidEnterBackground:.
+}
+
+@end
diff --git a/Firestore/Example/Firestore/FIRViewController.h b/Firestore/Example/Firestore/FIRViewController.h
new file mode 100644
index 0000000..64b4b74
--- /dev/null
+++ b/Firestore/Example/Firestore/FIRViewController.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import UIKit;
+
+@interface FIRViewController : UIViewController
+
+@end
diff --git a/Firestore/Example/Firestore/FIRViewController.m b/Firestore/Example/Firestore/FIRViewController.m
new file mode 100644
index 0000000..cdad545
--- /dev/null
+++ b/Firestore/Example/Firestore/FIRViewController.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 "FIRViewController.h"
+
+@interface FIRViewController ()
+
+@end
+
+@implementation FIRViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ // Do any additional setup after loading the view, typically from a nib.
+}
+
+- (void)didReceiveMemoryWarning {
+ [super didReceiveMemoryWarning];
+ // Dispose of any resources that can be recreated.
+}
+
+@end
diff --git a/Firestore/Example/Firestore/Firestore-Info.plist b/Firestore/Example/Firestore/Firestore-Info.plist
new file mode 100644
index 0000000..7576a0d
--- /dev/null
+++ b/Firestore/Example/Firestore/Firestore-Info.plist
@@ -0,0 +1,49 @@
+<?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>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json b/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d7070bc
--- /dev/null
+++ b/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,93 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "83.5x83.5",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/Firestore/Example/Firestore/en.lproj/InfoPlist.strings b/Firestore/Example/Firestore/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/Firestore/Example/Firestore/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/Firestore/Example/Firestore/main.m b/Firestore/Example/Firestore/main.m
new file mode 100644
index 0000000..724fccf
--- /dev/null
+++ b/Firestore/Example/Firestore/main.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 UIKit;
+#import "FIRAppDelegate.h"
+
+int main(int argc, char* argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class]));
+ }
+}
diff --git a/Firestore/Example/Podfile b/Firestore/Example/Podfile
new file mode 100644
index 0000000..4008af7
--- /dev/null
+++ b/Firestore/Example/Podfile
@@ -0,0 +1,22 @@
+use_frameworks!
+platform :ios, '8.0'
+
+target 'Firestore_Example' do
+ pod 'FirebaseCommunity/Core', :path => '../../'
+ pod 'Firestore', :path => '../'
+
+ target 'Firestore_Tests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ pod 'leveldb-library'
+ end
+
+ target 'Firestore_IntegrationTests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ end
+end
+
+target 'SwiftBuildTest' do
+ pod 'Firestore', :path => '../'
+end
diff --git a/Firestore/Example/SwiftBuildTest/main.swift b/Firestore/Example/SwiftBuildTest/main.swift
new file mode 100644
index 0000000..ff6c0dd
--- /dev/null
+++ b/Firestore/Example/SwiftBuildTest/main.swift
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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
+
+import Firestore
+
+func main() {
+ let db = initializeDb();
+
+ let (collectionRef, documentRef) = makeRefs(database: db);
+
+ let query = makeQuery(collection: collectionRef);
+
+ writeDocument(at: documentRef);
+
+ addDocument(to: collectionRef);
+
+ readDocument(at: documentRef);
+
+ readDocuments(matching: query);
+
+ listenToDocument(at: documentRef);
+
+ listenToDocuments(matching: query);
+
+ types();
+}
+
+func initializeDb() -> Firestore {
+
+ // Initialize with ProjectID.
+ let firestore = Firestore.firestore()
+
+ // Apply settings
+ let settings = FirestoreSettings()
+ settings.host = "localhost"
+ settings.isPersistenceEnabled = true
+ firestore.settings = settings
+
+ return firestore;
+}
+
+func makeRefs(database db: Firestore) -> (CollectionReference, DocumentReference) {
+
+ var collectionRef = db.collection("my-collection")
+
+ var documentRef: DocumentReference;
+ documentRef = collectionRef.document("my-doc")
+ // or
+ documentRef = db.document("my-collection/my-doc")
+
+ // deeper collection (my-collection/my-doc/some/deep/collection)
+ collectionRef = documentRef.collection("some/deep/collection")
+
+ // parent doc (my-collection/my-doc/some/deep)
+ documentRef = collectionRef.parent!
+
+ // print paths.
+ print("Collection: \(collectionRef.path), document: \(documentRef.path)")
+
+ return (collectionRef, documentRef);
+}
+
+func makeQuery(collection collectionRef: CollectionReference) -> Query {
+
+ let query = collectionRef.whereField(FieldPath(["name"]), isEqualTo: "Fred")
+ .whereField("age", isGreaterThanOrEqualTo: 24)
+ .whereField(FieldPath.documentID(), isEqualTo: "fred")
+ .order(by: FieldPath(["age"]))
+ .order(by: "name", descending: true)
+ .limit(to: 10)
+
+ return query;
+}
+
+func writeDocument(at docRef: DocumentReference) {
+
+ let setData = [
+ "foo": 42,
+ "bar": [
+ "baz": "Hello world!"
+ ]
+ ] as [String : Any];
+
+ let updateData = [
+ "bar.baz": 42,
+ FieldPath(["foobar"]) : 42
+ ] as [AnyHashable : Any];
+
+ docRef.setData(setData)
+
+ // Completion callback (via trailing closure syntax).
+ docRef.setData(setData) { error in
+ if let error = error {
+ print("Uh oh! \(error)")
+ return
+ }
+
+ print("Set complete!")
+ }
+
+ // SetOptions
+ docRef.setData(setData, options:SetOptions.merge())
+
+ docRef.updateData(updateData)
+ docRef.delete();
+
+ docRef.delete() { error in
+ if let error = error {
+ print("Uh oh! \(error)")
+ return
+ }
+
+ print("Set complete!")
+ }
+}
+
+func addDocument(to collectionRef: CollectionReference) {
+
+ collectionRef.addDocument(data: ["foo": 42]);
+ //or
+ collectionRef.document().setData(["foo": 42]);
+}
+
+func readDocument(at docRef: DocumentReference) {
+
+ // Trailing closure syntax.
+ docRef.getDocument() { document, error in
+ if let document = document {
+ // NOTE that document is nullable.
+ let data = document.data();
+ print("Read document: \(data)")
+
+ // Fields are read via subscript notation.
+ if let foo = document["foo"] {
+ print("Field: \(foo)")
+ }
+ } else {
+ // TODO(mikelehen): There may be a better way to do this, but it at least demonstrates
+ // the swift error domain / enum codes are renamed appropriately.
+ if let errorCode = error.flatMap({
+ ($0._domain == FirestoreErrorDomain) ? FirestoreErrorCode (rawValue: $0._code) : nil
+ }) {
+ switch errorCode {
+ case .unavailable:
+ print("Can't read document due to being offline!")
+ case _:
+ print("Failed to read.")
+ }
+ } else {
+ print("Unknown error!")
+ }
+ }
+
+ }
+}
+
+func readDocuments(matching query: Query) {
+ query.getDocuments() { querySnapshot, error in
+ // TODO(mikelehen): Figure out how to make "for..in" syntax work
+ // directly on documentSet.
+ for document in querySnapshot!.documents {
+ print(document.data())
+ }
+ }
+}
+
+func listenToDocument(at docRef: DocumentReference) {
+
+ let listener = docRef.addSnapshotListener() { document, error in
+ if let error = error {
+ print("Uh oh! Listen canceled: \(error)")
+ return
+ }
+
+ if let document = document {
+ print("Current document: \(document.data())");
+ if (document.metadata.isFromCache) {
+ print("From Cache")
+ } else {
+ print("From Server")
+ }
+ }
+ }
+
+ // Unsubscribe.
+ listener.remove();
+}
+
+func listenToDocuments(matching query: Query) {
+
+ let listener = query.addSnapshotListener() { snap, error in
+ if let error = error {
+ print("Uh oh! Listen canceled: \(error)")
+ return
+ }
+
+ if let snap = snap {
+ print("NEW SNAPSHOT (empty=\(snap.isEmpty) count=\(snap.count)")
+
+ // TODO(mikelehen): Figure out how to make "for..in" syntax work
+ // directly on documentSet.
+ for document in snap.documents {
+ print("Doc: ", document.data())
+ }
+ }
+ }
+
+ // Unsubscribe
+ listener.remove();
+}
+
+func listenToQueryDiffs(onQuery query: Query) {
+
+ let listener = query.addSnapshotListener() { snap, error in
+ if let snap = snap {
+ for change in snap.documentChanges {
+ switch (change.type) {
+ case .added:
+ print("New document: \(change.document.data())")
+ case .modified:
+ print("Modified document: \(change.document.data())")
+ case .removed:
+ print("Removed document: \(change.document.data())")
+ }
+ }
+ }
+ }
+
+ // Unsubscribe
+ listener.remove();
+}
+
+func transactions() {
+ let db = Firestore.firestore()
+
+ let collectionRef = db.collection("cities")
+ let accA = collectionRef.document("accountA")
+ let accB = collectionRef.document("accountB")
+ let amount = 20.0
+
+ db.runTransaction({ (transaction, errorPointer) -> Any? in
+ do {
+ let balanceA = try transaction.getDocument(accA)["balance"] as! Double
+ let balanceB = try transaction.getDocument(accB)["balance"] as! Double
+
+ if (balanceA < amount) {
+ errorPointer?.pointee = NSError(domain: "Foo", code: 123, userInfo: nil)
+ return nil
+ }
+ transaction.updateData(["balance": balanceA - amount], forDocument:accA)
+ transaction.updateData(["balance": balanceB + amount], forDocument:accB)
+ } catch let error as NSError {
+ print("Uh oh! \(error)")
+ }
+ return 0
+ }) { (result, error) in
+ // handle result.
+ }
+}
+
+func types() {
+ // Just highlighting the types of everything, though devs can/will often omit them.
+ let _: Firestore;
+ let _: CollectionReference;
+ let _: DocumentReference;
+ let _: Query;
+ let _: DocumentSnapshot;
+ let _: QuerySnapshot;
+}
diff --git a/Firestore/Example/Tests/API/FIRGeoPointTests.m b/Firestore/Example/Tests/API/FIRGeoPointTests.m
new file mode 100644
index 0000000..ca7d24c
--- /dev/null
+++ b/Firestore/Example/Tests/API/FIRGeoPointTests.m
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Firestore/FIRGeoPoint.h"
+
+#import <XCTest/XCTest.h>
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRGeoPointTests : XCTestCase
+@end
+
+@implementation FIRGeoPointTests
+
+- (void)testEquals {
+ XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0],
+ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0]);
+ XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56],
+ [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56]);
+ XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0],
+ [[FIRGeoPoint alloc] initWithLatitude:1 longitude:0]);
+ XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0],
+ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:1]);
+ XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0],
+ [[NSObject alloc] init]);
+}
+
+- (void)testComparison {
+ NSArray *values = @[
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:180] ],
+ ];
+
+ FSTAssertComparisons(values);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m
new file mode 100644
index 0000000..931a59f
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m
@@ -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 "Core/FSTDatabaseInfo.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTDatabaseID.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDatabaseInfoTests : XCTestCase
+@end
+
+@implementation FSTDatabaseInfoTests
+
+- (void)testConstructor {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"pk"
+ host:@"h"
+ sslEnabled:YES];
+ XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p");
+ XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"d");
+ XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk");
+ XCTAssertEqualObjects(databaseInfo.host, @"h");
+ XCTAssertEqual(databaseInfo.sslEnabled, YES);
+}
+
+- (void)testDefaultDatabase {
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID];
+ FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"pk"
+ host:@"h"
+ sslEnabled:YES];
+ XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p");
+ XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"(default)");
+ XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk");
+ XCTAssertEqualObjects(databaseInfo.host, @"h");
+ XCTAssertEqual(databaseInfo.sslEnabled, YES);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.m b/Firestore/Example/Tests/Core/FSTEventManagerTests.m
new file mode 100644
index 0000000..85afe58
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTEventManagerTests.m
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTEventManager.h"
+
+#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTSyncEngine.h"
+#import "Model/FSTDocumentSet.h"
+#import "Util/FSTDispatchQueue.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// FSTEventManager implements this delegate privately
+@interface FSTEventManager () <FSTSyncEngineDelegate>
+@end
+
+@interface FSTEventManagerTests : XCTestCase
+@end
+
+@implementation FSTEventManagerTests {
+ FSTDispatchQueue *_testUserQueue;
+}
+
+- (void)setUp {
+ _testUserQueue = [FSTDispatchQueue queueWith:dispatch_get_main_queue()];
+}
+
+- (FSTQueryListener *)noopListenerForQuery:(FSTQuery *)query {
+ return [[FSTQueryListener alloc]
+ initWithQuery:query
+ options:[FSTListenOptions defaultOptions]
+ viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error){
+ }];
+}
+
+- (void)testHandlesManyListenersPerQuery {
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")];
+ FSTQueryListener *listener1 = [self noopListenerForQuery:query];
+ FSTQueryListener *listener2 = [self noopListenerForQuery:query];
+
+ FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]);
+ OCMExpect([syncEngineMock setDelegate:[OCMArg any]]);
+ FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock];
+
+ OCMExpect([syncEngineMock listenToQuery:query]);
+ [eventManager addListener:listener1];
+ OCMVerifyAll((id)syncEngineMock);
+
+ [eventManager addListener:listener2];
+ [eventManager removeListener:listener2];
+
+ OCMExpect([syncEngineMock stopListeningToQuery:query]);
+ [eventManager removeListener:listener1];
+ OCMVerifyAll((id)syncEngineMock);
+}
+
+- (void)testHandlesUnlistenOnUnknownListenerGracefully {
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")];
+ FSTQueryListener *listener = [self noopListenerForQuery:query];
+
+ FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]);
+ OCMExpect([syncEngineMock setDelegate:[OCMArg any]]);
+ FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock];
+
+ [eventManager removeListener:listener];
+ OCMVerifyAll((id)syncEngineMock);
+}
+
+- (FSTQueryListener *)makeMockListenerForQuery:(FSTQuery *)query
+ viewSnapshotHandler:(void (^)())handler {
+ FSTQueryListener *listener = OCMClassMock([FSTQueryListener class]);
+ OCMStub([listener query]).andReturn(query);
+ OCMStub([listener queryDidChangeViewSnapshot:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
+ handler();
+ });
+ return listener;
+}
+
+- (void)testNotifiesListenersInTheRightOrder {
+ FSTQuery *query1 = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")];
+ FSTQuery *query2 = [FSTQuery queryWithPath:FSTTestPath(@"bar/baz")];
+ NSMutableArray *eventOrder = [NSMutableArray array];
+
+ FSTQueryListener *listener1 = [self makeMockListenerForQuery:query1
+ viewSnapshotHandler:^{
+ [eventOrder addObject:@"listener1"];
+ }];
+
+ FSTQueryListener *listener2 = [self makeMockListenerForQuery:query2
+ viewSnapshotHandler:^{
+ [eventOrder addObject:@"listener2"];
+ }];
+
+ FSTQueryListener *listener3 = [self makeMockListenerForQuery:query1
+ viewSnapshotHandler:^{
+ [eventOrder addObject:@"listener3"];
+ }];
+
+ FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]);
+ FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock];
+
+ [eventManager addListener:listener1];
+ [eventManager addListener:listener2];
+ [eventManager addListener:listener3];
+ OCMVerify([syncEngineMock listenToQuery:query1]);
+ OCMVerify([syncEngineMock listenToQuery:query2]);
+
+ FSTViewSnapshot *snapshot1 = OCMClassMock([FSTViewSnapshot class]);
+ OCMStub([snapshot1 query]).andReturn(query1);
+ FSTViewSnapshot *snapshot2 = OCMClassMock([FSTViewSnapshot class]);
+ OCMStub([snapshot2 query]).andReturn(query2);
+
+ [eventManager handleViewSnapshots:@[ snapshot1, snapshot2 ]];
+
+ NSArray *expected = @[ @"listener1", @"listener3", @"listener2" ];
+ XCTAssertEqualObjects(eventOrder, expected);
+}
+
+- (void)testWillForwardOnlineStateChanges {
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")];
+ FSTQueryListener *fakeListener = OCMClassMock([FSTQueryListener class]);
+ NSMutableArray *events = [NSMutableArray array];
+ OCMStub([fakeListener query]).andReturn(query);
+ OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateUnknown])
+ .andDo(^(NSInvocation *invocation) {
+ [events addObject:@(FSTOnlineStateUnknown)];
+ });
+ OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateHealthy])
+ .andDo(^(NSInvocation *invocation) {
+ [events addObject:@(FSTOnlineStateHealthy)];
+ });
+
+ FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]);
+ OCMExpect([syncEngineMock setDelegate:[OCMArg any]]);
+ FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock];
+
+ [eventManager addListener:fakeListener];
+ XCTAssertEqualObjects(events, @[ @(FSTOnlineStateUnknown) ]);
+ [eventManager watchStreamDidChangeOnlineState:FSTOnlineStateHealthy];
+ XCTAssertEqualObjects(events, (@[ @(FSTOnlineStateUnknown), @(FSTOnlineStateHealthy) ]));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.m b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m
new file mode 100644
index 0000000..155eb00
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTEventManager.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTView.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentSet.h"
+#import "Remote/FSTRemoteEvent.h"
+#import "Util/FSTAsyncQueryListener.h"
+#import "Util/FSTDispatchQueue.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTQueryListenerTests : XCTestCase
+@property(nonatomic, strong, readonly) FSTDispatchQueue *asyncQueue;
+@end
+
+@implementation FSTQueryListenerTests
+
+- (void)setUp {
+ _asyncQueue = [FSTDispatchQueue
+ queueWith:dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)];
+}
+
+- (void)testRaisesCollectionEvents {
+ NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array];
+ NSMutableArray<FSTViewSnapshot *> *otherAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTDocument *doc2prime =
+ FSTTestDoc(@"rooms/Hades", 3, @{@"name" : @"Hades", @"owner" : @"Jonny"}, NO);
+
+ FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum];
+ FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil);
+
+ FSTDocumentViewChange *change1 =
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change2 =
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change3 =
+ [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeModified];
+ FSTDocumentViewChange *change4 =
+ [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeAdded];
+
+ [listener queryDidChangeViewSnapshot:snap1];
+ [listener queryDidChangeViewSnapshot:snap2];
+ [otherListener queryDidChangeViewSnapshot:snap2];
+
+ XCTAssertEqualObjects(accum, (@[ snap1, snap2 ]));
+ XCTAssertEqualObjects(accum[0].documentChanges, (@[ change1, change2 ]));
+ XCTAssertEqualObjects(accum[1].documentChanges, (@[ change3 ]));
+
+ FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc]
+ initWithQuery:snap2.query
+ documents:snap2.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap2.query.comparator]
+ documentChanges:@[ change1, change4 ]
+ fromCache:snap2.fromCache
+ hasPendingWrites:snap2.hasPendingWrites
+ syncStateChanged:YES];
+ XCTAssertEqualObjects(otherAccum, (@[ expectedSnap2 ]));
+}
+
+- (void)testRaisesErrorEvent {
+ NSMutableArray<NSError *> *accum = [NSMutableArray array];
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")];
+
+ FSTQueryListener *listener = [self listenToQuery:query
+ handler:^(FSTViewSnapshot *snapshot, NSError *error) {
+ [accum addObject:error];
+ }];
+
+ NSError *testError =
+ [NSError errorWithDomain:@"com.google.firestore.test" code:42 userInfo:@{@"some" : @"info"}];
+ [listener queryDidError:testError];
+
+ XCTAssertEqualObjects(accum, @[ testError ]);
+}
+
+- (void)testRaisesEventForEmptyCollectionAfterSync {
+ NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array];
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+
+ FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil);
+
+ FSTTargetChange *ackTarget =
+ [FSTTargetChange changeWithDocuments:@[]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget);
+
+ [listener queryDidChangeViewSnapshot:snap1];
+ XCTAssertEqualObjects(accum, @[]);
+
+ [listener queryDidChangeViewSnapshot:snap2];
+ XCTAssertEqualObjects(accum, @[ snap2 ]);
+}
+
+- (void)testMutingAsyncListenerPreventsAllSubsequentEvents {
+ NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 3, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Eros", 4, @{@"name" : @"Eros2"}, NO);
+
+ __block FSTAsyncQueryListener *listener = [[FSTAsyncQueryListener alloc]
+ initWithDispatchQueue:self.asyncQueue
+ snapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) {
+ [accum addObject:snapshot];
+ [listener mute];
+ }];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
+ FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil);
+
+ FSTViewSnapshotHandler handler = listener.asyncSnapshotHandler;
+ handler(viewSnapshot1, nil);
+ handler(viewSnapshot2, nil);
+
+ // Drain queue
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Queue drained"];
+ [self.asyncQueue dispatchAsync:^{
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:4.0
+ handler:^(NSError *_Nullable expectationError) {
+ if (expectationError) {
+ XCTFail(@"Error waiting for timeout: %@", expectationError);
+ }
+ }];
+
+ // We should get the first snapshot but not the second.
+ XCTAssertEqualObjects(accum, @[ viewSnapshot1 ]);
+}
+
+- (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified {
+ NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array];
+ NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+
+ FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES
+ includeDocumentMetadataChanges:NO
+ waitForSyncWhenOnline:NO];
+
+ FSTQueryListener *filteredListener =
+ [self listenToQuery:query accumulatingSnapshots:filteredAccum];
+ FSTQueryListener *fullListener =
+ [self listenToQuery:query options:options accumulatingSnapshots:fullAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
+
+ FSTTargetChange *ackTarget =
+ [FSTTargetChange changeWithDocuments:@[ doc1 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget);
+ FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil);
+
+ [filteredListener queryDidChangeViewSnapshot:snap1]; // local event
+ [filteredListener queryDidChangeViewSnapshot:snap2]; // no event
+ [filteredListener queryDidChangeViewSnapshot:snap3]; // doc2 update
+
+ [fullListener queryDidChangeViewSnapshot:snap1]; // local event
+ [fullListener queryDidChangeViewSnapshot:snap2]; // state change event
+ [fullListener queryDidChangeViewSnapshot:snap3]; // doc2 update
+
+ XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ]));
+ XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ]));
+}
+
+- (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified {
+ NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array];
+ NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO);
+
+ FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO
+ includeDocumentMetadataChanges:YES
+ waitForSyncWhenOnline:NO];
+
+ FSTQueryListener *filteredListener =
+ [self listenToQuery:query accumulatingSnapshots:filteredAccum];
+ FSTQueryListener *fullListener =
+ [self listenToQuery:query options:options accumulatingSnapshots:fullAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil);
+ FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil);
+
+ FSTDocumentViewChange *change1 =
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change2 =
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change3 =
+ [FSTDocumentViewChange changeWithDocument:doc1Prime type:FSTDocumentViewChangeTypeMetadata];
+ FSTDocumentViewChange *change4 =
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded];
+
+ [filteredListener queryDidChangeViewSnapshot:snap1];
+ [filteredListener queryDidChangeViewSnapshot:snap2];
+ [filteredListener queryDidChangeViewSnapshot:snap3];
+ [fullListener queryDidChangeViewSnapshot:snap1];
+ [fullListener queryDidChangeViewSnapshot:snap2];
+ [fullListener queryDidChangeViewSnapshot:snap3];
+
+ XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ]));
+ XCTAssertEqualObjects(filteredAccum[0].documentChanges, (@[ change1, change2 ]));
+ XCTAssertEqualObjects(filteredAccum[1].documentChanges, (@[ change4 ]));
+
+ XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ]));
+ XCTAssertEqualObjects(fullAccum[0].documentChanges, (@[ change1, change2 ]));
+ XCTAssertEqualObjects(fullAccum[1].documentChanges, (@[ change3 ]));
+ XCTAssertEqualObjects(fullAccum[2].documentChanges, (@[ change4 ]));
+}
+
+- (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges {
+ NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, YES);
+ FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2Prime = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO);
+
+ FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES
+ includeDocumentMetadataChanges:NO
+ waitForSyncWhenOnline:NO];
+ FSTQueryListener *fullListener =
+ [self listenToQuery:query options:options accumulatingSnapshots:fullAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil);
+ FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil);
+ FSTViewSnapshot *snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], nil);
+
+ [fullListener queryDidChangeViewSnapshot:snap1];
+ [fullListener queryDidChangeViewSnapshot:snap2]; // Emits no events.
+ [fullListener queryDidChangeViewSnapshot:snap3];
+ [fullListener queryDidChangeViewSnapshot:snap4]; // Metadata change event.
+
+ FSTViewSnapshot *expectedSnap4 = [[FSTViewSnapshot alloc] initWithQuery:snap4.query
+ documents:snap4.documents
+ oldDocuments:snap3.documents
+ documentChanges:@[]
+ fromCache:snap4.fromCache
+ hasPendingWrites:NO
+ syncStateChanged:snap4.syncStateChanged];
+ XCTAssertEqualObjects(fullAccum, (@[ snap1, snap3, expectedSnap4 ]));
+}
+
+- (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadataChangesIsFalse {
+ NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO);
+
+ FSTQueryListener *filteredListener =
+ [self listenToQuery:query accumulatingSnapshots:filteredAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil);
+
+ FSTDocumentViewChange *change3 =
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded];
+
+ [filteredListener queryDidChangeViewSnapshot:snap1];
+ [filteredListener queryDidChangeViewSnapshot:snap2];
+
+ FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:snap2.query
+ documents:snap2.documents
+ oldDocuments:snap1.documents
+ documentChanges:@[ change3 ]
+ fromCache:snap2.isFromCache
+ hasPendingWrites:snap2.hasPendingWrites
+ syncStateChanged:snap2.syncStateChanged];
+ XCTAssertEqualObjects(filteredAccum, (@[ snap1, expectedSnap2 ]));
+}
+
+- (void)testWillWaitForSyncIfOnline {
+ NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTQueryListener *listener =
+ [self listenToQuery:query
+ options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO
+ includeDocumentMetadataChanges:NO
+ waitForSyncWhenOnline:YES]
+ accumulatingSnapshots:events];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil);
+ FSTViewSnapshot *snap3 =
+ FSTTestApplyChanges(view, @[],
+ [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+
+ [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event
+ [listener queryDidChangeViewSnapshot:snap1];
+ [listener clientDidChangeOnlineState:FSTOnlineStateUnknown];
+ [listener clientDidChangeOnlineState:FSTOnlineStateHealthy];
+ [listener queryDidChangeViewSnapshot:snap2];
+ [listener queryDidChangeViewSnapshot:snap3];
+
+ FSTDocumentViewChange *change1 =
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change2 =
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded];
+ FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc]
+ initWithQuery:snap3.query
+ documents:snap3.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap3.query.comparator]
+ documentChanges:@[ change1, change2 ]
+ fromCache:NO
+ hasPendingWrites:NO
+ syncStateChanged:YES];
+ XCTAssertEqualObjects(events, (@[ expectedSnap ]));
+}
+
+- (void)testWillRaiseInitialEventWhenGoingOffline {
+ NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTQueryListener *listener =
+ [self listenToQuery:query
+ options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO
+ includeDocumentMetadataChanges:NO
+ waitForSyncWhenOnline:YES]
+ accumulatingSnapshots:events];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil);
+
+ [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event
+ [listener queryDidChangeViewSnapshot:snap1]; // no event
+ [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event
+ [listener clientDidChangeOnlineState:FSTOnlineStateUnknown]; // no event
+ [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event
+ [listener queryDidChangeViewSnapshot:snap2]; // another event
+
+ FSTDocumentViewChange *change1 =
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change2 =
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded];
+ FSTViewSnapshot *expectedSnap1 = [[FSTViewSnapshot alloc]
+ initWithQuery:query
+ documents:snap1.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator]
+ documentChanges:@[ change1 ]
+ fromCache:YES
+ hasPendingWrites:NO
+ syncStateChanged:YES];
+ FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:query
+ documents:snap2.documents
+ oldDocuments:snap1.documents
+ documentChanges:@[ change2 ]
+ fromCache:YES
+ hasPendingWrites:NO
+ syncStateChanged:NO];
+ XCTAssertEqualObjects(events, (@[ expectedSnap1, expectedSnap2 ]));
+}
+
+- (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs {
+ NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTQueryListener *listener = [self listenToQuery:query
+ options:[FSTListenOptions defaultOptions]
+ accumulatingSnapshots:events];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil);
+
+ [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event
+ [listener queryDidChangeViewSnapshot:snap1]; // no event
+ [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event
+
+ FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc]
+ initWithQuery:query
+ documents:snap1.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator]
+ documentChanges:@[]
+ fromCache:YES
+ hasPendingWrites:NO
+ syncStateChanged:YES];
+ XCTAssertEqualObjects(events, (@[ expectedSnap ]));
+}
+
+- (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs {
+ NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTQueryListener *listener = [self listenToQuery:query
+ options:[FSTListenOptions defaultOptions]
+ accumulatingSnapshots:events];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil);
+
+ [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event
+ [listener queryDidChangeViewSnapshot:snap1]; // event
+
+ FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc]
+ initWithQuery:query
+ documents:snap1.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator]
+ documentChanges:@[]
+ fromCache:YES
+ hasPendingWrites:NO
+ syncStateChanged:YES];
+ XCTAssertEqualObjects(events, (@[ expectedSnap ]));
+}
+
+- (FSTQueryListener *)listenToQuery:(FSTQuery *)query handler:(FSTViewSnapshotHandler)handler {
+ return [[FSTQueryListener alloc] initWithQuery:query
+ options:[FSTListenOptions defaultOptions]
+ viewSnapshotHandler:handler];
+}
+
+- (FSTQueryListener *)listenToQuery:(FSTQuery *)query
+ options:(FSTListenOptions *)options
+ accumulatingSnapshots:(NSMutableArray<FSTViewSnapshot *> *)values {
+ return [[FSTQueryListener alloc] initWithQuery:query
+ options:options
+ viewSnapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) {
+ [values addObject:snapshot];
+ }];
+}
+
+- (FSTQueryListener *)listenToQuery:(FSTQuery *)query
+ accumulatingSnapshots:(NSMutableArray<FSTViewSnapshot *> *)values {
+ return [self listenToQuery:query
+ options:[FSTListenOptions defaultOptions]
+ accumulatingSnapshots:values];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTQueryTests.m b/Firestore/Example/Tests/Core/FSTQueryTests.m
new file mode 100644
index 0000000..051f10d
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTQueryTests.m
@@ -0,0 +1,577 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTQuery.h"
+
+#import <XCTest/XCTest.h>
+
+#import "API/FIRFirestore+Internal.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Convenience methods for building test queries. */
+@interface FSTQuery (Tests)
+- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending;
+@end
+
+@implementation FSTQuery (Tests)
+
+- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending {
+ return [self queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(key)
+ ascending:ascending]];
+}
+
+@end
+
+@interface FSTQueryTests : XCTestCase
+@end
+
+@implementation FSTQueryTests
+
+- (void)testConstructor {
+ FSTResourcePath *path =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages", @"0001" ]];
+ FSTQuery *query = [FSTQuery queryWithPath:path];
+ XCTAssertNotNil(query);
+
+ XCTAssertEqual(query.sortOrders.count, 1);
+ XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, kDocumentKeyPath);
+ XCTAssertEqual(query.sortOrders[0].ascending, YES);
+
+ XCTAssertEqual(query.explicitSortOrders.count, 0);
+}
+
+- (void)testOrderBy {
+ FSTResourcePath *path =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages" ]];
+ FSTQuery *query = [FSTQuery queryWithPath:path];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"length")
+ ascending:NO]];
+
+ XCTAssertEqual(query.sortOrders.count, 2);
+ XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, @"length");
+ XCTAssertEqual(query.sortOrders[0].ascending, NO);
+ XCTAssertEqualObjects(query.sortOrders[1].field.canonicalString, kDocumentKeyPath);
+ XCTAssertEqual(query.sortOrders[1].ascending, NO);
+
+ XCTAssertEqual(query.explicitSortOrders.count, 1);
+ XCTAssertEqualObjects(query.explicitSortOrders[0].field.canonicalString, @"length");
+ XCTAssertEqual(query.explicitSortOrders[0].ascending, NO);
+}
+
+- (void)testMatchesBasedOnDocumentKey {
+ FSTResourcePath *queryKey =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages", @"1" ]];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO);
+
+ // document query
+ FSTQuery *query = [FSTQuery queryWithPath:queryKey];
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc2]);
+ XCTAssertFalse([query matchesDocument:doc3]);
+}
+
+- (void)testMatchesCorrectlyForShallowAncestorQuery {
+ FSTResourcePath *queryPath =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc1Meta = FSTTestDoc(@"rooms/eros/messages/1/meta/1", 0, @{@"meta" : @"mv"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO);
+
+ // shallow ancestor query
+ FSTQuery *query = [FSTQuery queryWithPath:queryPath];
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc1Meta]);
+ XCTAssertTrue([query matchesDocument:doc2]);
+ XCTAssertFalse([query matchesDocument:doc3]);
+}
+
+- (void)testEmptyFieldsAreAllowedForQueries {
+ FSTResourcePath *queryPath = [FSTResourcePath pathWithString:@"rooms/eros/messages"];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO);
+
+ FSTQuery *query = [[FSTQuery queryWithPath:queryPath]
+ queryByAddingFilter:FSTTestFilter(@"text", @"==", @"msg1")];
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc2]);
+}
+
+- (void)testMatchesPrimitiveValuesForFilters {
+ FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))];
+ FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))];
+
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO);
+ FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO);
+
+ XCTAssertFalse([query1 matchesDocument:doc1]);
+ XCTAssertTrue([query1 matchesDocument:doc2]);
+ XCTAssertTrue([query1 matchesDocument:doc3]);
+ XCTAssertFalse([query1 matchesDocument:doc4]);
+ XCTAssertFalse([query1 matchesDocument:doc5]);
+ XCTAssertFalse([query1 matchesDocument:doc6]);
+
+ XCTAssertTrue([query2 matchesDocument:doc1]);
+ XCTAssertTrue([query2 matchesDocument:doc2]);
+ XCTAssertFalse([query2 matchesDocument:doc3]);
+ XCTAssertFalse([query2 matchesDocument:doc4]);
+ XCTAssertFalse([query2 matchesDocument:doc5]);
+ XCTAssertFalse([query2 matchesDocument:doc6]);
+}
+
+- (void)testNullFilter {
+ FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @"==", [NSNull null])];
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{@"sort" : [NSNull null]}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO);
+
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc2]);
+ XCTAssertFalse([query matchesDocument:doc3]);
+ XCTAssertFalse([query matchesDocument:doc4]);
+ XCTAssertFalse([query matchesDocument:doc5]);
+}
+
+- (void)testNanFilter {
+ FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @"==", @(NAN))];
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @(NAN) }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO);
+
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc2]);
+ XCTAssertFalse([query matchesDocument:doc3]);
+ XCTAssertFalse([query matchesDocument:doc4]);
+ XCTAssertFalse([query matchesDocument:doc5]);
+}
+
+- (void)testDoesNotMatchComplexObjectsForFilters {
+ FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))];
+ FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))];
+
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO);
+ FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{ @"sort" : @{} }, NO); // no sort field
+ FSTDocument *doc7 = FSTTestDoc(@"collection/7", 0, @{ @"sort" : @[ @3, @1 ] }, NO);
+
+ XCTAssertTrue([query1 matchesDocument:doc1]);
+ XCTAssertFalse([query1 matchesDocument:doc2]);
+ XCTAssertFalse([query1 matchesDocument:doc3]);
+ XCTAssertFalse([query1 matchesDocument:doc4]);
+ XCTAssertFalse([query1 matchesDocument:doc5]);
+ XCTAssertFalse([query1 matchesDocument:doc6]);
+ XCTAssertFalse([query1 matchesDocument:doc7]);
+
+ XCTAssertTrue([query2 matchesDocument:doc1]);
+ XCTAssertFalse([query2 matchesDocument:doc2]);
+ XCTAssertFalse([query2 matchesDocument:doc3]);
+ XCTAssertFalse([query2 matchesDocument:doc4]);
+ XCTAssertFalse([query2 matchesDocument:doc5]);
+ XCTAssertFalse([query2 matchesDocument:doc6]);
+ XCTAssertFalse([query2 matchesDocument:doc7]);
+}
+
+- (void)testDoesntRemoveComplexObjectsWithOrderBy {
+ FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort")
+ ascending:YES]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO);
+ FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO);
+
+ XCTAssertTrue([query1 matchesDocument:doc1]);
+ XCTAssertTrue([query1 matchesDocument:doc2]);
+ XCTAssertTrue([query1 matchesDocument:doc3]);
+ XCTAssertTrue([query1 matchesDocument:doc4]);
+ XCTAssertTrue([query1 matchesDocument:doc5]);
+ XCTAssertFalse([query1 matchesDocument:doc6]);
+}
+
+- (void)testFiltersBasedOnArrayValue {
+ FSTQuery *baseQuery =
+ [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"collection/doc", 0, @{ @"tags" : @[ @"foo", @1, @YES ] }, NO);
+
+ NSArray<id<FSTFilter>> *matchingFilters =
+ @[ FSTTestFilter(@"tags", @"==", @[ @"foo", @1, @YES ]) ];
+
+ NSArray<id<FSTFilter>> *nonMatchingFilters = @[
+ FSTTestFilter(@"tags", @"==", @"foo"),
+ FSTTestFilter(@"tags", @"==", @[ @"foo", @1 ]),
+ FSTTestFilter(@"tags", @"==", @[ @"foo", @YES, @1 ]),
+ ];
+
+ for (id<FSTFilter> filter in matchingFilters) {
+ XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]);
+ }
+
+ for (id<FSTFilter> filter in nonMatchingFilters) {
+ XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]);
+ }
+}
+
+- (void)testFiltersBasedOnObjectValue {
+ FSTQuery *baseQuery =
+ [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+
+ FSTDocument *doc1 =
+ FSTTestDoc(@"collection/doc", 0,
+ @{ @"tags" : @{@"foo" : @"foo", @"a" : @0, @"b" : @YES, @"c" : @(NAN)} }, NO);
+
+ NSArray<id<FSTFilter>> *matchingFilters = @[
+ FSTTestFilter(@"tags", @"==",
+ @{ @"foo" : @"foo",
+ @"a" : @0,
+ @"b" : @YES,
+ @"c" : @(NAN) }),
+ FSTTestFilter(@"tags", @"==",
+ @{ @"b" : @YES,
+ @"a" : @0,
+ @"foo" : @"foo",
+ @"c" : @(NAN) }),
+ FSTTestFilter(@"tags.foo", @"==", @"foo")
+ ];
+
+ NSArray<id<FSTFilter>> *nonMatchingFilters = @[
+ FSTTestFilter(@"tags", @"==", @"foo"), FSTTestFilter(@"tags", @"==", @{
+ @"foo" : @"foo",
+ @"a" : @0,
+ @"b" : @YES,
+ })
+ ];
+
+ for (id<FSTFilter> filter in matchingFilters) {
+ XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]);
+ }
+
+ for (id<FSTFilter> filter in nonMatchingFilters) {
+ XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]);
+ }
+}
+
+/**
+ * Checks that an ordered array of elements yields the correct pair-wise comparison result for the
+ * supplied comparator.
+ */
+- (void)assertCorrectComparisonsWithArray:(NSArray *)array comparator:(NSComparator)comp {
+ [array enumerateObjectsUsingBlock:^(id iObj, NSUInteger i, BOOL *outerStop) {
+ [array enumerateObjectsUsingBlock:^(id _Nonnull jObj, NSUInteger j, BOOL *innerStop) {
+ NSComparisonResult expected = [@(i) compare:@(j)];
+ NSComparisonResult actual = comp(iObj, jObj);
+ XCTAssertEqual(actual, expected, @"Compared %@ to %@ at (%lu, %lu).", iObj, jObj,
+ (unsigned long)i, (unsigned long)j);
+ }];
+ }];
+}
+
+- (void)testSortsDocumentsInTheCorrectOrder {
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort")
+ ascending:YES]];
+
+ // clang-format off
+ NSArray<FSTDocument *> *docs = @[
+ FSTTestDoc(@"collection/1", 0, @{@"sort": [NSNull null]}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @NO}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @YES}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @1}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort": @1}, NO), // by key
+ FSTTestDoc(@"collection/3", 0, @{@"sort": @1}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @1.9}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @2}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @2.1}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @""}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @"a"}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @"ab"}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @"b"}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort":
+ FSTTestRef(@"project", kDefaultDatabaseID, @"collection/id1")}, NO),
+ ];
+ // clang-format on
+
+ [self assertCorrectComparisonsWithArray:docs comparator:query.comparator];
+}
+
+- (void)testSortsDocumentsUsingMultipleFields {
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1")
+ ascending:YES]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2")
+ ascending:YES]];
+
+ // clang-format off
+ NSArray<FSTDocument *> *docs =
+ @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO),
+ ];
+ // clang-format on
+
+ [self assertCorrectComparisonsWithArray:docs comparator:query.comparator];
+}
+
+- (void)testSortsDocumentsWithDescendingToo {
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1")
+ ascending:NO]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2")
+ ascending:NO]];
+
+ // clang-format off
+ NSArray<FSTDocument *> *docs =
+ @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO),
+ FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO),
+ FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO),
+ ];
+ // clang-format on
+
+ [self assertCorrectComparisonsWithArray:docs comparator:query.comparator];
+}
+
+- (void)testEquality {
+ FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))];
+ q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))];
+ FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))];
+ q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))];
+
+ FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+
+ FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+ FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+
+ FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES];
+ q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES];
+ FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES];
+ q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES];
+ FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES];
+ q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES];
+
+ FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES];
+ q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))];
+ FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))];
+ q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES];
+ FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))];
+ q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES];
+
+ FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q61 = [q61 queryBySettingLimit:10];
+
+ // XCTAssertEqualObjects(q11, q12); // TODO(klimt): not canonical yet
+ XCTAssertNotEqualObjects(q11, q21);
+ XCTAssertNotEqualObjects(q11, q31);
+ XCTAssertNotEqualObjects(q11, q41);
+ XCTAssertNotEqualObjects(q11, q51);
+ XCTAssertNotEqualObjects(q11, q61);
+
+ XCTAssertEqualObjects(q21, q22);
+ XCTAssertNotEqualObjects(q21, q31);
+ XCTAssertNotEqualObjects(q21, q41);
+ XCTAssertNotEqualObjects(q21, q51);
+ XCTAssertNotEqualObjects(q21, q61);
+
+ XCTAssertEqualObjects(q31, q32);
+ XCTAssertNotEqualObjects(q31, q41);
+ XCTAssertNotEqualObjects(q31, q51);
+ XCTAssertNotEqualObjects(q31, q61);
+
+ XCTAssertEqualObjects(q41, q42);
+ XCTAssertNotEqualObjects(q41, q43Diff);
+ XCTAssertNotEqualObjects(q41, q51);
+ XCTAssertNotEqualObjects(q41, q61);
+
+ XCTAssertEqualObjects(q51, q52);
+ XCTAssertNotEqualObjects(q51, q53Diff);
+ XCTAssertNotEqualObjects(q51, q61);
+}
+
+- (void)testUniqueIds {
+ FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))];
+ q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))];
+ FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))];
+ q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))];
+
+ FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+
+ FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+ FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+
+ FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES];
+ q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES];
+ FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES];
+ q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES];
+ FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES];
+ q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES];
+
+ FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES];
+ q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))];
+ FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))];
+ q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES];
+ FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))];
+ q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES];
+
+ FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q61 = [q61 queryBySettingLimit:10];
+
+ // XCTAssertEqual(q11.hash, q12.hash); // TODO(klimt): not canonical yet
+ XCTAssertNotEqual(q11.hash, q21.hash);
+ XCTAssertNotEqual(q11.hash, q31.hash);
+ XCTAssertNotEqual(q11.hash, q41.hash);
+ XCTAssertNotEqual(q11.hash, q51.hash);
+ XCTAssertNotEqual(q11.hash, q61.hash);
+
+ XCTAssertEqual(q21.hash, q22.hash);
+ XCTAssertNotEqual(q21.hash, q31.hash);
+ XCTAssertNotEqual(q21.hash, q41.hash);
+ XCTAssertNotEqual(q21.hash, q51.hash);
+ XCTAssertNotEqual(q21.hash, q61.hash);
+
+ XCTAssertEqual(q31.hash, q32.hash);
+ XCTAssertNotEqual(q31.hash, q41.hash);
+ XCTAssertNotEqual(q31.hash, q51.hash);
+ XCTAssertNotEqual(q31.hash, q61.hash);
+
+ XCTAssertEqual(q41.hash, q42.hash);
+ XCTAssertNotEqual(q41.hash, q43Diff.hash);
+ XCTAssertNotEqual(q41.hash, q51.hash);
+ XCTAssertNotEqual(q41.hash, q61.hash);
+
+ XCTAssertEqual(q51.hash, q52.hash);
+ XCTAssertNotEqual(q51.hash, q53Diff.hash);
+ XCTAssertNotEqual(q51.hash, q61.hash);
+}
+
+- (void)testImplicitOrderBy {
+ FSTQuery *baseQuery = FSTTestQuery(@"foo");
+ // Default is ascending
+ XCTAssertEqualObjects(baseQuery.sortOrders, @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]);
+
+ // Explicit key ordering is respected
+ XCTAssertEqualObjects(
+ [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")].sortOrders,
+ @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]);
+ XCTAssertEqualObjects(
+ [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")].sortOrders,
+ @[ FSTTestOrderBy(kDocumentKeyPath, @"desc") ]);
+
+ XCTAssertEqualObjects(
+ [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")]
+ queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")]
+ .sortOrders,
+ (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ]));
+
+ XCTAssertEqualObjects(
+ [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")]
+ queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")]
+ .sortOrders,
+ (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ]));
+
+ // Inequality filters add order bys
+ XCTAssertEqualObjects(
+ [baseQuery queryByAddingFilter:FSTTestFilter(@"foo", @"<", @5)].sortOrders,
+ (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ]));
+
+ // Descending order by applies to implicit key ordering
+ XCTAssertEqualObjects(
+ [baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")].sortOrders,
+ (@[ FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ]));
+ XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")]
+ queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"desc")]
+ .sortOrders,
+ (@[
+ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(@"bar", @"desc"),
+ FSTTestOrderBy(kDocumentKeyPath, @"desc")
+ ]));
+ XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")]
+ queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"asc")]
+ .sortOrders,
+ (@[
+ FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(@"bar", @"asc"),
+ FSTTestOrderBy(kDocumentKeyPath, @"asc")
+ ]));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h b/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h
new file mode 100644
index 0000000..ff98e74
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.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 "Core/FSTSyncEngine.h"
+
+@class FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTSyncEngine (Testing)
+
+/** Returns the current set of limbo document keys and their associated target IDs. */
+- (NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m b/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m
new file mode 100644
index 0000000..11a7f46
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.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 "Core/FSTTargetIDGenerator.h"
+
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTTargetIDGenerator ()
+- (instancetype)initWithGeneratorID:(NSInteger)generatorID startingAfterID:(FSTTargetID)after;
+@end
+
+@interface FSTTargetIDGeneratorTests : XCTestCase
+@end
+
+@implementation FSTTargetIDGeneratorTests
+
+- (void)testConstructor {
+ XCTAssertEqual([[[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:0] nextID],
+ 2);
+ XCTAssertEqual([[[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:0] nextID],
+ 1);
+
+ XCTAssertEqual([[FSTTargetIDGenerator generatorForLocalStoreStartingAfterID:0] nextID], 2);
+ XCTAssertEqual([[FSTTargetIDGenerator generatorForSyncEngineStartingAfterID:0] nextID], 1);
+}
+
+- (void)testSkipPast {
+ FSTTargetIDGenerator *gen =
+ [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:-1];
+ XCTAssertEqual([gen nextID], 1);
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:2];
+ XCTAssertEqual([gen nextID], 3);
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:4];
+ XCTAssertEqual([gen nextID], 5);
+
+ for (int i = 4; i < 12; ++i) {
+ FSTTargetIDGenerator *gen0 =
+ [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:i];
+ FSTTargetIDGenerator *gen1 =
+ [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:i];
+ XCTAssertEqual([gen0 nextID], i + 2 & ~1, @"Skip failed for index %d", i);
+ XCTAssertEqual([gen1 nextID], i + 1 | 1, @"Skip failed for index %d", i);
+ }
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:12];
+ XCTAssertEqual([gen nextID], 13);
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:22];
+ XCTAssertEqual([gen nextID], 24);
+}
+
+- (void)testIncrement {
+ FSTTargetIDGenerator *gen =
+ [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:0];
+ XCTAssertEqual([gen nextID], 2);
+ XCTAssertEqual([gen nextID], 4);
+ XCTAssertEqual([gen nextID], 6);
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:46];
+ XCTAssertEqual([gen nextID], 48);
+ XCTAssertEqual([gen nextID], 50);
+ XCTAssertEqual([gen nextID], 52);
+ XCTAssertEqual([gen nextID], 54);
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:0];
+ XCTAssertEqual([gen nextID], 1);
+ XCTAssertEqual([gen nextID], 3);
+ XCTAssertEqual([gen nextID], 5);
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:46];
+ XCTAssertEqual([gen nextID], 47);
+ XCTAssertEqual([gen nextID], 49);
+ XCTAssertEqual([gen nextID], 51);
+ XCTAssertEqual([gen nextID], 53);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTTimestampTests.m b/Firestore/Example/Tests/Core/FSTTimestampTests.m
new file mode 100644
index 0000000..8538078
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTTimestampTests.m
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTTimestamp.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Util/FSTAssert.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTTimestampTests : XCTestCase
+@end
+
+@implementation FSTTimestampTests
+
+- (void)testFromDate {
+ // Very carefully construct an NSDate that won't lose precision with its milliseconds.
+ NSDate *input = [NSDate dateWithTimeIntervalSinceReferenceDate:1.5];
+
+ FSTTimestamp *actual = [FSTTimestamp timestampWithDate:input];
+ static const int64_t kSecondsFromEpochToReferenceDate = 978307200;
+ XCTAssertEqual(kSecondsFromEpochToReferenceDate + 1, actual.seconds);
+ XCTAssertEqual(500000000, actual.nanos);
+
+ FSTTimestamp *expected =
+ [[FSTTimestamp alloc] initWithSeconds:(kSecondsFromEpochToReferenceDate + 1) nanos:500000000];
+ XCTAssertEqualObjects(expected, actual);
+}
+
+- (void)testSO8601String {
+ NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0);
+ FSTTimestamp *timestamp =
+ [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:543000000];
+ XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.543000000Z");
+}
+
+- (void)testISO8601String_withLowMilliseconds {
+ NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0);
+ FSTTimestamp *timestamp =
+ [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:7000000];
+ XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.007000000Z");
+}
+
+- (void)testISO8601String_withLowNanos {
+ FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:1];
+ XCTAssertEqualObjects(timestamp.ISO8601String, @"1970-01-01T00:00:00.000000001Z");
+}
+
+- (void)testISO8601String_withNegativeSeconds {
+ FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:-1 nanos:999999999];
+ XCTAssertEqualObjects(timestamp.ISO8601String, @"1969-12-31T23:59:59.999999999Z");
+}
+
+- (void)testCompare {
+ NSArray<FSTTimestamp *> *timestamps = @[
+ [[FSTTimestamp alloc] initWithSeconds:12344 nanos:999999999],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:0],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:000000001],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:99999999],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000000],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000001],
+ [[FSTTimestamp alloc] initWithSeconds:12346 nanos:0],
+ ];
+ for (int i = 0; i < timestamps.count - 1; ++i) {
+ XCTAssertEqual(NSOrderedAscending, [timestamps[i] compare:timestamps[i + 1]]);
+ XCTAssertEqual(NSOrderedDescending, [timestamps[i + 1] compare:timestamps[i]]);
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m
new file mode 100644
index 0000000..202fae8
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.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 "Core/FSTViewSnapshot.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentSet.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTViewSnapshotTests : XCTestCase
+@end
+
+@implementation FSTViewSnapshotTests
+
+- (void)testDocumentChangeConstructor {
+ FSTDocument *doc = FSTTestDoc(@"a/b", 0, @{}, NO);
+ FSTDocumentViewChangeType type = FSTDocumentViewChangeTypeModified;
+ FSTDocumentViewChange *change = [FSTDocumentViewChange changeWithDocument:doc type:type];
+ XCTAssertEqual(change.document, doc);
+ XCTAssertEqual(change.type, type);
+}
+
+- (void)testTrack {
+ FSTDocumentViewChangeSet *set = [FSTDocumentViewChangeSet changeSet];
+
+ FSTDocument *docAdded = FSTTestDoc(@"a/1", 0, @{}, NO);
+ FSTDocument *docRemoved = FSTTestDoc(@"a/2", 0, @{}, NO);
+ FSTDocument *docModified = FSTTestDoc(@"a/3", 0, @{}, NO);
+
+ FSTDocument *docAddedThenModified = FSTTestDoc(@"b/1", 0, @{}, NO);
+ FSTDocument *docAddedThenRemoved = FSTTestDoc(@"b/2", 0, @{}, NO);
+ FSTDocument *docRemovedThenAdded = FSTTestDoc(@"b/3", 0, @{}, NO);
+ FSTDocument *docModifiedThenRemoved = FSTTestDoc(@"b/4", 0, @{}, NO);
+ FSTDocument *docModifiedThenModified = FSTTestDoc(@"b/5", 0, @{}, NO);
+
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAdded
+ type:FSTDocumentViewChangeTypeAdded]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docRemoved
+ type:FSTDocumentViewChangeTypeRemoved]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModified
+ type:FSTDocumentViewChangeTypeModified]];
+
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified
+ type:FSTDocumentViewChangeTypeAdded]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified
+ type:FSTDocumentViewChangeTypeModified]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved
+ type:FSTDocumentViewChangeTypeAdded]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved
+ type:FSTDocumentViewChangeTypeRemoved]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded
+ type:FSTDocumentViewChangeTypeRemoved]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded
+ type:FSTDocumentViewChangeTypeAdded]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved
+ type:FSTDocumentViewChangeTypeModified]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved
+ type:FSTDocumentViewChangeTypeRemoved]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified
+ type:FSTDocumentViewChangeTypeModified]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified
+ type:FSTDocumentViewChangeTypeModified]];
+
+ NSArray<FSTDocumentViewChange *> *changes = [set changes];
+ XCTAssertEqual(changes.count, 7);
+
+ XCTAssertEqual(changes[0].document, docAdded);
+ XCTAssertEqual(changes[0].type, FSTDocumentViewChangeTypeAdded);
+
+ XCTAssertEqual(changes[1].document, docRemoved);
+ XCTAssertEqual(changes[1].type, FSTDocumentViewChangeTypeRemoved);
+
+ XCTAssertEqual(changes[2].document, docModified);
+ XCTAssertEqual(changes[2].type, FSTDocumentViewChangeTypeModified);
+
+ XCTAssertEqual(changes[3].document, docAddedThenModified);
+ XCTAssertEqual(changes[3].type, FSTDocumentViewChangeTypeAdded);
+
+ XCTAssertEqual(changes[4].document, docRemovedThenAdded);
+ XCTAssertEqual(changes[4].type, FSTDocumentViewChangeTypeModified);
+
+ XCTAssertEqual(changes[5].document, docModifiedThenRemoved);
+ XCTAssertEqual(changes[5].type, FSTDocumentViewChangeTypeRemoved);
+
+ XCTAssertEqual(changes[6].document, docModifiedThenModified);
+ XCTAssertEqual(changes[6].type, FSTDocumentViewChangeTypeModified);
+}
+
+- (void)testViewSnapshotConstructor {
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"a" ]]];
+ FSTDocumentSet *documents = [FSTDocumentSet documentSetWithComparator:FSTDocumentComparatorByKey];
+ FSTDocumentSet *oldDocuments = documents;
+ documents = [documents documentSetByAddingDocument:FSTTestDoc(@"c/a", 1, @{}, NO)];
+ NSArray<FSTDocumentViewChange *> *documentChanges =
+ @[ [FSTDocumentViewChange changeWithDocument:FSTTestDoc(@"c/a", 1, @{}, NO)
+ type:FSTDocumentViewChangeTypeAdded] ];
+
+ BOOL fromCache = YES;
+ BOOL hasPendingWrites = NO;
+ BOOL syncStateChanged = YES;
+
+ FSTViewSnapshot *snapshot = [[FSTViewSnapshot alloc] initWithQuery:query
+ documents:documents
+ oldDocuments:oldDocuments
+ documentChanges:documentChanges
+ fromCache:fromCache
+ hasPendingWrites:hasPendingWrites
+ syncStateChanged:syncStateChanged];
+
+ XCTAssertEqual(snapshot.query, query);
+ XCTAssertEqual(snapshot.documents, documents);
+ XCTAssertEqual(snapshot.oldDocuments, oldDocuments);
+ XCTAssertEqual(snapshot.documentChanges, documentChanges);
+ XCTAssertEqual(snapshot.fromCache, fromCache);
+ XCTAssertEqual(snapshot.hasPendingWrites, hasPendingWrites);
+ XCTAssertEqual(snapshot.syncStateChanged, syncStateChanged);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTViewTests.m b/Firestore/Example/Tests/Core/FSTViewTests.m
new file mode 100644
index 0000000..d939d62
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTViewTests.m
@@ -0,0 +1,618 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTView.h"
+
+#import <XCTest/XCTest.h>
+
+#import "API/FIRFirestore+Internal.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTViewSnapshot.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTDocumentSet.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTRemoteEvent.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTViewTests : XCTestCase
+@end
+
+@implementation FSTViewTests
+
+/** Returns a new empty query to use for testing. */
+- (FSTQuery *)queryForMessages {
+ return [FSTQuery
+ queryWithPath:[FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]];
+}
+
+- (void)testAddsDocumentsBasedOnQuery {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO);
+
+ FSTViewSnapshot *_Nullable snapshot =
+ FSTTestApplyChanges(view, @[ doc1, doc2, doc3 ],
+ [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded],
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertFalse(snapshot.isFromCache);
+ XCTAssertFalse(snapshot.hasPendingWrites);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testRemovesDocuments {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO);
+
+ // initial state
+ FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+
+ // delete doc2, add doc3
+ FSTViewSnapshot *snapshot =
+ FSTTestApplyChanges(view, @[ FSTTestDeletedDoc(@"rooms/eros/messages/2", 0), doc3 ],
+ [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved],
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertFalse(snapshot.isFromCache);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testReturnsNilIfThereAreNoChanges {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+
+ // initial state
+ FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+
+ // reapply same docs, no changes
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ XCTAssertNil(snapshot);
+}
+
+- (void)testDoesNotReturnNilForFirstChanges {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil);
+ XCTAssertNotNil(snapshot);
+}
+
+- (void)testFiltersDocumentsBasedOnQueryWithFilter {
+ FSTQuery *query = [self queryForMessages];
+ FSTRelationFilter *filter =
+ [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort")
+ filterOperator:FSTRelationFilterOperatorLessThanOrEqual
+ value:[FSTDoubleValue doubleValue:2]];
+ query = [query queryByAddingFilter:filter];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); // no sort, no match
+ FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/5", 0, @{ @"sort" : @1 }, NO);
+
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], nil);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc5, doc2 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded],
+ [FSTDocumentViewChange changeWithDocument:doc5 type:FSTDocumentViewChangeTypeAdded],
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertTrue(snapshot.isFromCache);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testUpdatesDocumentsBasedOnQueryWithFilter {
+ FSTQuery *query = [self queryForMessages];
+ FSTRelationFilter *filter =
+ [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort")
+ filterOperator:FSTRelationFilterOperatorLessThanOrEqual
+ value:[FSTDoubleValue doubleValue:2]];
+ query = [query queryByAddingFilter:filter];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @3 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO);
+
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], nil);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ]));
+
+ FSTDocument *newDoc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"sort" : @2 }, NO);
+ FSTDocument *newDoc3 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"sort" : @3 }, NO);
+ FSTDocument *newDoc4 = FSTTestDoc(@"rooms/eros/messages/4", 1, @{ @"sort" : @0 }, NO);
+
+ snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], nil);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ newDoc4, doc1, newDoc2 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved],
+ [FSTDocumentViewChange changeWithDocument:newDoc4 type:FSTDocumentViewChangeTypeAdded],
+ [FSTDocumentViewChange changeWithDocument:newDoc2 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertTrue(snapshot.isFromCache);
+ XCTAssertFalse(snapshot.syncStateChanged);
+}
+
+- (void)testRemovesDocumentsForQueryWithLimit {
+ FSTQuery *query = [self queryForMessages];
+ query = [query queryBySettingLimit:2];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO);
+
+ // initial state
+ FSTTestApplyChanges(view, @[ doc1, doc3 ], nil);
+
+ // add doc2, which should push out doc3
+ FSTViewSnapshot *snapshot =
+ FSTTestApplyChanges(view, @[ doc2 ],
+ [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved],
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertFalse(snapshot.isFromCache);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery {
+ FSTQuery *query = [self queryForMessages];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"num")
+ ascending:YES]];
+ query = [query queryBySettingLimit:2];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"num" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"num" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"num" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"num" : @4 }, NO);
+
+ // initial state
+ FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+
+ // change doc2 to 5, and add doc3 and doc4.
+ // doc2 will be modified + removed = removed
+ // doc3 will be added
+ // doc4 will be added + removed = nothing
+ doc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"num" : @5 }, NO);
+ FSTViewDocumentChanges *viewDocChanges =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2, doc3, doc4 ])];
+ XCTAssertTrue(viewDocChanges.needsRefill);
+ // Verify that all the docs still match.
+ viewDocChanges = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4 ])
+ previousChanges:viewDocChanges];
+ FSTViewSnapshot *snapshot =
+ [view applyChangesToDocuments:viewDocChanges
+ targetChange:[FSTTargetChange
+ changeWithDocuments:@[ doc1, doc2, doc3, doc4 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]
+ .snapshot;
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved],
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertFalse(snapshot.isFromCache);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testKeepsTrackOfLimboDocuments {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO);
+
+ FSTViewChange *change = [view
+ applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]];
+ XCTAssertEqualObjects(change.limboChanges, @[]);
+
+ change =
+ [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
+ targetChange:[FSTTargetChange
+ changeWithDocuments:@[]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]];
+ XCTAssertEqualObjects(
+ change.limboChanges,
+ @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]);
+
+ change = [view
+ applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
+ targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateNone]];
+ XCTAssertEqualObjects(
+ change.limboChanges,
+ @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]);
+
+ change = [view
+ applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]
+ targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateNone]];
+ XCTAssertEqualObjects(change.limboChanges, @[]);
+
+ change = [view
+ applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]];
+ XCTAssertEqualObjects(
+ change.limboChanges,
+ @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc3.key] ]);
+
+ change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[
+ FSTTestDeletedDoc(@"rooms/eros/messages/2",
+ 1)
+ ])]]; // remove
+ XCTAssertEqualObjects(
+ change.limboChanges,
+ @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc3.key] ]);
+}
+
+- (void)testResumingQueryCreatesNoLimbos {
+ FSTQuery *query = [self queryForMessages];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+
+ // Unlike other cases, here the view is initialized with a set of previously synced documents
+ // which happens when listening to a previously listened-to query.
+ FSTView *view = [[FSTView alloc] initWithQuery:query
+ remoteDocuments:FSTTestDocKeySet(@[ doc1.key, doc2.key ])];
+
+ FSTTargetChange *markCurrent =
+ [FSTTargetChange changeWithDocuments:@[]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
+ FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])];
+ FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent];
+ XCTAssertEqualObjects(change.limboChanges, @[]);
+}
+
+- (void)assertDocSet:(FSTDocumentSet *)docSet containsDocs:(NSArray<FSTDocument *> *)docs {
+ XCTAssertEqual(docs.count, docSet.count);
+ for (FSTDocument *doc in docs) {
+ XCTAssertTrue([docSet containsKey:doc.key]);
+ }
+}
+
+- (void)testReturnsNeedsRefillOnDeleteInLimitQuery {
+ FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Remove one of the docs.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc(
+ @"rooms/eros/messages/0", 0) ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]];
+ XCTAssertTrue(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ // Refill it with just the one doc remaining.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ]) previousChanges:changes];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testReturnsNeedsRefillOnReorderInLimitQuery {
+ FSTQuery *query = [self queryForMessages];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order")
+ ascending:YES]];
+ query = [query queryBySettingLimit:2];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Move one of the docs.
+ doc2 = FSTTestDoc(@"rooms/eros/messages/1", 1, @{ @"order" : @2000 }, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertTrue(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ // Refill it with all three current docs.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])
+ previousChanges:changes];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc3 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testDoesntNeedRefillOnReorderWithinLimit {
+ FSTQuery *query = [self queryForMessages];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order")
+ ascending:YES]];
+ query = [query queryBySettingLimit:3];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(3, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Move one of the docs.
+ doc1 = FSTTestDoc(@"rooms/eros/messages/0", 1, @{ @"order" : @3 }, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc2, doc3, doc1 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testDoesntNeedRefillOnReorderAfterLimitQuery {
+ FSTQuery *query = [self queryForMessages];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order")
+ ascending:YES]];
+ query = [query queryBySettingLimit:3];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(3, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Move one of the docs.
+ doc4 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"order" : @6 }, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc4 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(0, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testDoesntNeedRefillForAdditionAfterTheLimit {
+ FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Add a doc that is past the limit.
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{}, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(0, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testDoesntNeedRefillForDeletionsWhenNotNearTheLimit {
+ FSTQuery *query = [[self queryForMessages] queryBySettingLimit:20];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Remove one of the docs.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc(
+ @"rooms/eros/messages/1", 0) ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testHandlesApplyingIrrelevantDocs {
+ FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Remove a doc that isn't even in the results.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc(
+ @"rooms/eros/messages/2", 0) ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(0, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testComputesMutatedKeys {
+ FSTQuery *query = [self queryForMessages];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[]));
+
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, YES);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc3.key ]));
+}
+
+- (void)testRemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges {
+ FSTQuery *query = [self queryForMessages];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+
+ FSTDocument *doc2Prime = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2Prime ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[]));
+}
+
+- (void)testRemembersLocalMutationsFromPreviousSnapshot {
+ FSTQuery *query = [self queryForMessages];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+}
+
+- (void)testRemembersLocalMutationsFromPreviousCallToComputeChangesWithDocuments {
+ FSTQuery *query = [self queryForMessages];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ]) previousChanges:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Integration/API/FIRCursorTests.m b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m
new file mode 100644
index 0000000..2f8babd
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRCursorTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRCursorTests
+
+- (void)testCanPageThroughItems {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a"},
+ @"b" : @{@"v" : @"b"},
+ @"c" : @{@"v" : @"c"},
+ @"d" : @{@"v" : @"d"},
+ @"e" : @{@"v" : @"e"},
+ @"f" : @{@"v" : @"f"}
+ }];
+
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[testCollection queryLimitedTo:2]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"v" : @"a"}, @{@"v" : @"b"} ]));
+
+ FIRDocumentSnapshot *lastDoc = snapshot.documents.lastObject;
+ snapshot = [self
+ readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot),
+ (@[ @{@"v" : @"c"}, @{@"v" : @"d"}, @{@"v" : @"e"} ]));
+
+ lastDoc = snapshot.documents.lastObject;
+ snapshot = [self
+ readDocumentSetForRef:[[testCollection queryLimitedTo:1] queryStartingAfterDocument:lastDoc]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[ @{@"v" : @"f"} ]);
+
+ lastDoc = snapshot.documents.lastObject;
+ snapshot = [self
+ readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[]);
+}
+
+- (void)testCanBeCreatedFromDocuments {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a", @"sort" : @1.0},
+ @"b" : @{@"v" : @"b", @"sort" : @2.0},
+ @"c" : @{@"v" : @"c", @"sort" : @2.0},
+ @"d" : @{@"v" : @"d", @"sort" : @2.0},
+ @"e" : @{@"v" : @"e", @"sort" : @0.0},
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up
+ }];
+
+ FIRQuery *query = [testCollection queryOrderedByField:@"sort"];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"c"]];
+
+ XCTAssertTrue(snapshot.exists);
+ FIRQuerySnapshot *querySnapshot =
+ [self readDocumentSetForRef:[query queryStartingAtDocument:snapshot]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"c",
+ @"sort" : @2.0 },
+ @{ @"v" : @"d",
+ @"sort" : @2.0 }
+ ]));
+
+ querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeDocument:snapshot]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"e",
+ @"sort" : @0.0 },
+ @{ @"v" : @"a",
+ @"sort" : @1.0 },
+ @{ @"v" : @"b",
+ @"sort" : @2.0 }
+ ]));
+}
+
+- (void)testCanBeCreatedFromValues {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a", @"sort" : @1.0},
+ @"b" : @{@"v" : @"b", @"sort" : @2.0},
+ @"c" : @{@"v" : @"c", @"sort" : @2.0},
+ @"d" : @{@"v" : @"d", @"sort" : @2.0},
+ @"e" : @{@"v" : @"e", @"sort" : @0.0},
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up
+ }];
+
+ FIRQuery *query = [testCollection queryOrderedByField:@"sort"];
+ FIRQuerySnapshot *querySnapshot =
+ [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"b",
+ @"sort" : @2.0 },
+ @{ @"v" : @"c",
+ @"sort" : @2.0 },
+ @{ @"v" : @"d",
+ @"sort" : @2.0 }
+ ]));
+
+ querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"e",
+ @"sort" : @0.0 },
+ @{ @"v" : @"a",
+ @"sort" : @1.0 }
+ ]));
+}
+
+- (void)testCanBeCreatedUsingDocumentId {
+ NSDictionary *testDocs = @{
+ @"a" : @{@"k" : @"a"},
+ @"b" : @{@"k" : @"b"},
+ @"c" : @{@"k" : @"c"},
+ @"d" : @{@"k" : @"d"},
+ @"e" : @{@"k" : @"e"}
+ };
+ FIRCollectionReference *writer = [[[[self firestore] collectionWithPath:@"parent-collection"]
+ documentWithAutoID] collectionWithPath:@"sub-collection"];
+ [self writeAllDocuments:testDocs toCollection:writer];
+
+ FIRCollectionReference *reader = [[self firestore] collectionWithPath:writer.path];
+ FIRQuerySnapshot *querySnapshot =
+ [self readDocumentSetForRef:[[[reader queryOrderedByFieldPath:[FIRFieldPath documentID]]
+ queryStartingAtValues:@[ @"b" ]]
+ queryEndingBeforeValues:@[ @"d" ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot),
+ (@[ @{@"k" : @"b"}, @{@"k" : @"c"} ]));
+}
+
+- (void)testCanBeUsedWithReferenceValues {
+ FIRFirestore *db = [self firestore];
+
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"k" : @"1a", @"ref" : [db documentWithPath:@"1/a"]},
+ @"b" : @{@"k" : @"1b", @"ref" : [db documentWithPath:@"1/b"]},
+ @"c" : @{@"k" : @"2a", @"ref" : [db documentWithPath:@"2/a"]},
+ @"d" : @{@"k" : @"2b", @"ref" : [db documentWithPath:@"2/b"]},
+ @"e" : @{@"k" : @"3a", @"ref" : [db documentWithPath:@"3/a"]},
+ }];
+ FIRQuery *query = [testCollection queryOrderedByField:@"ref"];
+ FIRQuerySnapshot *querySnapshot = [self
+ readDocumentSetForRef:[[query queryStartingAfterValues:@[ [db documentWithPath:@"1/a"] ]]
+ queryEndingAtValues:@[ [db documentWithPath:@"2/b"] ]]];
+ NSMutableArray<NSString *> *actual = [NSMutableArray array];
+ [querySnapshot.documents enumerateObjectsUsingBlock:^(FIRDocumentSnapshot *_Nonnull doc,
+ NSUInteger idx, BOOL *_Nonnull stop) {
+ [actual addObject:doc.data[@"k"]];
+ }];
+ XCTAssertEqualObjects(actual, (@[ @"1b", @"2a", @"2b" ]));
+}
+
+- (void)testCanBeUsedInDescendingQueries {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a", @"sort" : @1.0},
+ @"b" : @{@"v" : @"b", @"sort" : @2.0},
+ @"c" : @{@"v" : @"c", @"sort" : @2.0},
+ @"d" : @{@"v" : @"d", @"sort" : @3.0},
+ @"e" : @{@"v" : @"e", @"sort" : @0.0},
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up
+ }];
+ FIRQuery *query = [[testCollection queryOrderedByField:@"sort" descending:YES]
+ queryOrderedByFieldPath:[FIRFieldPath documentID]
+ descending:YES];
+
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
+ @{ @"v" : @"c",
+ @"sort" : @2.0 },
+ @{ @"v" : @"b",
+ @"sort" : @2.0 },
+ @{ @"v" : @"a",
+ @"sort" : @1.0 },
+ @{ @"v" : @"e",
+ @"sort" : @0.0 }
+ ]));
+
+ snapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{ @"v" : @"d", @"sort" : @3.0 } ]));
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m
new file mode 100644
index 0000000..d5558cc
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m
@@ -0,0 +1,741 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRDatabaseTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRDatabaseTests
+
+- (void)testCanUpdateAnExistingDocument {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+ NSDictionary<NSString *, id> *initialData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} };
+ NSDictionary<NSString *, id> *updateData =
+ @{@"desc" : @"NewDescription", @"owner.email" : @"new@xyz.com"};
+ NSDictionary<NSString *, id> *finalData =
+ @{ @"desc" : @"NewDescription",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"new@xyz.com"} };
+
+ [self writeDocumentRef:doc data:initialData];
+
+ XCTestExpectation *updateCompletion = [self expectationWithDescription:@"updateData"];
+ [doc updateData:updateData
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [updateCompletion fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertTrue(result.exists);
+ XCTAssertEqualObjects(result.data, finalData);
+}
+
+- (void)testCanDeleteAFieldWithAnUpdate {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+ NSDictionary<NSString *, id> *initialData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} };
+ NSDictionary<NSString *, id> *updateData =
+ @{@"owner.email" : [FIRFieldValue fieldValueForDelete]};
+ NSDictionary<NSString *, id> *finalData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny"} };
+
+ [self writeDocumentRef:doc data:initialData];
+ [self updateDocumentRef:doc data:updateData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertTrue(result.exists);
+ XCTAssertEqualObjects(result.data, finalData);
+}
+
+- (void)testDeleteDocument {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+ NSDictionary<NSString *, id> *data = @{@"value" : @"foo"};
+ [self writeDocumentRef:doc data:data];
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, data);
+ [self deleteDocumentRef:doc];
+ result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testCannotUpdateNonexistentDocument {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *setCompletion = [self expectationWithDescription:@"setData"];
+ [doc updateData:@{@"owner" : @"abc"}
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
+ [setCompletion fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testCanOverwriteDataAnExistingDocumentUsingSet {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} };
+ NSDictionary<NSString *, id> *udpateData = @{@"desc" : @"NewDescription"};
+
+ [self writeDocumentRef:doc data:initialData];
+ [self writeDocumentRef:doc data:udpateData];
+
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(document.data, udpateData);
+}
+
+- (void)testCanMergeDataWithAnExistingDocumentUsingSet {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{
+ @"desc" : @"Description",
+ @"owner.data" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"}
+ };
+ NSDictionary<NSString *, id> *updateData =
+ @{ @"updated" : @YES,
+ @"owner.data" : @{@"name" : @"Sebastian"} };
+ NSDictionary<NSString *, id> *finalData = @{
+ @"desc" : @"Description",
+ @"updated" : @YES,
+ @"owner.data" : @{@"name" : @"Sebastian", @"email" : @"abc@xyz.com"}
+ };
+
+ [self writeDocumentRef:doc data:initialData];
+
+ XCTestExpectation *completed =
+ [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"];
+
+ [doc setData:updateData
+ options:[FIRSetOptions merge]
+ completion:^(NSError *error) {
+ XCTAssertNil(error);
+ [completed fulfill];
+ }];
+
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(document.data, finalData);
+}
+
+- (void)testMergeReplacesArrays {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{
+ @"untouched" : @YES,
+ @"data" : @"old",
+ @"topLevel" : @[ @"old", @"old" ],
+ @"mapInArray" : @[ @{@"data" : @"old"} ]
+ };
+ NSDictionary<NSString *, id> *updateData =
+ @{ @"data" : @"new",
+ @"topLevel" : @[ @"new" ],
+ @"mapInArray" : @[ @{@"data" : @"new"} ] };
+ NSDictionary<NSString *, id> *finalData = @{
+ @"untouched" : @YES,
+ @"data" : @"new",
+ @"topLevel" : @[ @"new" ],
+ @"mapInArray" : @[ @{@"data" : @"new"} ]
+ };
+
+ [self writeDocumentRef:doc data:initialData];
+
+ XCTestExpectation *completed =
+ [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"];
+
+ [doc setData:updateData
+ options:[FIRSetOptions merge]
+ completion:^(NSError *error) {
+ XCTAssertNil(error);
+ [completed fulfill];
+ }];
+
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(document.data, finalData);
+}
+
+- (void)testAddingToACollectionYieldsTheCorrectDocumentReference {
+ FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
+ FIRDocumentReference *ref = [coll addDocumentWithData:@{ @"foo" : @1 }];
+
+ XCTestExpectation *getCompletion = [self expectationWithDescription:@"getData"];
+ [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable document,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(document.data, (@{ @"foo" : @1 }));
+
+ [getCompletion fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testListenCanBeCalledMultipleTimes {
+ FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
+ FIRDocumentReference *doc = [coll documentWithAutoID];
+
+ XCTestExpectation *completed = [self expectationWithDescription:@"multiple addSnapshotListeners"];
+
+ __block NSDictionary<NSString *, id> *resultingData;
+
+ // Shut the compiler up about strong references to doc.
+ FIRDocumentReference *__weak weakDoc = doc;
+
+ [doc setData:@{@"foo" : @"bar"}
+ completion:^(NSError *error1) {
+ XCTAssertNil(error1);
+ FIRDocumentReference *strongDoc = weakDoc;
+
+ [strongDoc addSnapshotListener:^(FIRDocumentSnapshot *snapshot2, NSError *error2) {
+ XCTAssertNil(error2);
+
+ FIRDocumentReference *strongDoc2 = weakDoc;
+ [strongDoc2 addSnapshotListener:^(FIRDocumentSnapshot *snapshot3, NSError *error3) {
+ XCTAssertNil(error3);
+ resultingData = snapshot3.data;
+ [completed fulfill];
+ }];
+ }];
+ }];
+
+ [self awaitExpectations];
+ XCTAssertEqualObjects(resultingData, @{@"foo" : @"bar"});
+}
+
+- (void)testDocumentSnapshotEvents_nonExistent {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *snapshotCompletion = [self expectationWithDescription:@"snapshot"];
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertNotNil(doc);
+ XCTAssertFalse(doc.exists);
+ [snapshotCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forAdd {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"];
+ __block XCTestExpectation *dataCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertNotNil(doc);
+ XCTAssertFalse(doc.exists);
+ [emptyCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 }));
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+ [dataCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ dataCompletion = [self expectationWithDescription:@"data snapshot"];
+
+ [docRef setData:@{ @"a" : @1 }];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forAddIncludingMetadata {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"];
+ __block XCTestExpectation *dataCompletion;
+ __block int callbacks = 0;
+
+ FIRDocumentListenOptions *options =
+ [[FIRDocumentListenOptions options] includeMetadataChanges:YES];
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListenerWithOptions:options
+ listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertNotNil(doc);
+ XCTAssertFalse(doc.exists);
+ [emptyCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 }));
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+
+ } else if (callbacks == 3) {
+ XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 }));
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ [dataCompletion fulfill];
+
+ } else if (callbacks == 4) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ dataCompletion = [self expectationWithDescription:@"data snapshot"];
+
+ [docRef setData:@{ @"a" : @1 }];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forChange {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+ NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, changedData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef setData:changedData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forChangeIncludingMetadata {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+ NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ FIRDocumentListenOptions *options =
+ [[FIRDocumentListenOptions options] includeMetadataChanges:YES];
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListenerWithOptions:options
+ listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, YES);
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTAssertEqualObjects(doc.data, changedData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+
+ } else if (callbacks == 4) {
+ XCTAssertEqualObjects(doc.data, changedData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 5) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef setData:changedData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forDelete {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, YES);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertFalse(doc.exists);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef deleteDocument];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forDeleteIncludingMetadata {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ FIRDocumentListenOptions *options =
+ [[FIRDocumentListenOptions options] includeMetadataChanges:YES];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListenerWithOptions:options
+ listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, YES);
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTAssertFalse(doc.exists);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 4) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef deleteDocument];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testQuerySnapshotEvents_forAdd {
+ FIRCollectionReference *roomsRef = [self collectionRef];
+ FIRDocumentReference *docRef = [roomsRef documentWithAutoID];
+
+ NSDictionary<NSString *, id> *newData = @{ @"a" : @1 };
+
+ XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqual(docSet.count, 0);
+ [emptyCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, newData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received a third callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"changed snapshot"];
+
+ [docRef setData:newData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testQuerySnapshotEvents_forChange {
+ FIRCollectionReference *roomsRef = [self collectionRef];
+ FIRDocumentReference *docRef = [roomsRef documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+ NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, initialData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, changedData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received a third callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef setData:changedData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testQuerySnapshotEvents_forDelete {
+ FIRCollectionReference *roomsRef = [self collectionRef];
+ FIRDocumentReference *docRef = [roomsRef documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, initialData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqual(docSet.count, 0);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 4) {
+ XCTFail("Should not have received a third callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef deleteDocument];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testExposesFirestoreOnDocumentReferences {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"foo/bar"];
+ XCTAssertEqual(doc.firestore, self.db);
+}
+
+- (void)testExposesFirestoreOnQueries {
+ FIRQuery *q = [[self.db collectionWithPath:@"foo"] queryLimitedTo:5];
+ XCTAssertEqual(q.firestore, self.db);
+}
+
+- (void)testCanTraverseCollectionsAndDocuments {
+ NSString *expected = @"a/b/c/d";
+ // doc path from root Firestore.
+ XCTAssertEqualObjects([self.db documentWithPath:@"a/b/c/d"].path, expected);
+ // collection path from root Firestore.
+ XCTAssertEqualObjects([[self.db collectionWithPath:@"a/b/c"] documentWithPath:@"d"].path,
+ expected);
+ // doc path from CollectionReference.
+ XCTAssertEqualObjects([[self.db collectionWithPath:@"a"] documentWithPath:@"b/c/d"].path,
+ expected);
+ // collection path from DocumentReference.
+ XCTAssertEqualObjects([[self.db documentWithPath:@"a/b"] collectionWithPath:@"c/d/e"].path,
+ @"a/b/c/d/e");
+}
+
+- (void)testCanTraverseCollectionAndDocumentParents {
+ FIRCollectionReference *collection = [self.db collectionWithPath:@"a/b/c"];
+ XCTAssertEqualObjects(collection.path, @"a/b/c");
+
+ FIRDocumentReference *doc = collection.parent;
+ XCTAssertEqualObjects(doc.path, @"a/b");
+
+ collection = doc.parent;
+ XCTAssertEqualObjects(collection.path, @"a");
+
+ FIRDocumentReference *nilDoc = collection.parent;
+ XCTAssertNil(nilDoc);
+}
+
+- (void)testUpdateFieldsWithDots {
+ FIRDocumentReference *doc = [self documentRef];
+
+ [self writeDocumentRef:doc data:@{@"a.b" : @"old", @"c.d" : @"old"}];
+
+ [self updateDocumentRef:doc data:@{ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"];
+
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"}));
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testUpdateNestedFields {
+ FIRDocumentReference *doc = [self documentRef];
+
+ [self writeDocumentRef:doc
+ data:@{
+ @"a" : @{@"b" : @"old"},
+ @"c" : @{@"d" : @"old"},
+ @"e" : @{@"f" : @"old"}
+ }];
+
+ [self updateDocumentRef:doc
+ data:@{
+ @"a.b" : @"new",
+ [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"
+ }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"];
+
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{
+ @"a" : @{@"b" : @"new"},
+ @"c" : @{@"d" : @"new"},
+ @"e" : @{@"f" : @"old"}
+ }));
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testCollectionID {
+ XCTAssertEqualObjects([self.db collectionWithPath:@"foo"].collectionID, @"foo");
+ XCTAssertEqualObjects([self.db collectionWithPath:@"foo/bar/baz"].collectionID, @"baz");
+}
+
+- (void)testDocumentID {
+ XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar"].documentID, @"bar");
+ XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar/baz/qux"].documentID, @"qux");
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m
new file mode 100644
index 0000000..d851556
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTFirestoreClient.h"
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRFieldsTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRFieldsTests
+
+- (NSDictionary<NSString *, id> *)testNestedDataNumbered:(int)number {
+ return @{
+ @"name" : [NSString stringWithFormat:@"room %d", number],
+ @"metadata" : @{
+ @"createdAt" : @(number),
+ @"deep" : @{@"field" : [NSString stringWithFormat:@"deep-field-%d", number]}
+ }
+ };
+}
+
+- (void)testNestedFieldsCanBeWrittenWithSet {
+ NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, testData);
+}
+
+- (void)testNestedFieldsCanBeReadDirectly {
+ NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result[@"name"], testData[@"name"]);
+ XCTAssertEqualObjects(result[@"metadata"], testData[@"metadata"]);
+ XCTAssertEqualObjects(result[@"metadata.deep.field"], testData[@"metadata"][@"deep"][@"field"]);
+ XCTAssertNil(result[@"metadata.nofield"]);
+ XCTAssertNil(result[@"nometadata.nofield"]);
+}
+
+- (void)testNestedFieldsCanBeUpdated {
+ NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+ [self updateDocumentRef:doc data:@{ @"metadata.deep.field" : @100, @"metadata.added" : @200 }];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(
+ result.data, (@{
+ @"name" : @"room 1",
+ @"metadata" : @{@"createdAt" : @1, @"deep" : @{@"field" : @100}, @"added" : @200}
+ }));
+}
+
+- (void)testNestedFieldsCanBeUsedInQueryFilters {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testNestedDataNumbered:300],
+ @"2" : [self testNestedDataNumbered:100],
+ @"3" : [self testNestedDataNumbered:200]
+ };
+
+ // inequality adds implicit sort on field
+ NSArray<NSDictionary<NSString *, id> *> *expected =
+ @[ [self testNestedDataNumbered:200], [self testNestedDataNumbered:300] ];
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ FIRQuery *q = [coll queryWhereField:@"metadata.createdAt" isGreaterThanOrEqualTo:@200];
+ FIRQuerySnapshot *results = [self readDocumentSetForRef:q];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (expected));
+}
+
+- (void)testNestedFieldsCanBeUsedInOrderBy {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testNestedDataNumbered:300],
+ @"2" : [self testNestedDataNumbered:100],
+ @"3" : [self testNestedDataNumbered:200]
+ };
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"];
+ FIRQuery *q = [coll queryOrderedByField:@"metadata.createdAt"];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[
+ [self testNestedDataNumbered:100], [self testNestedDataNumbered:200],
+ [self testNestedDataNumbered:300]
+ ]));
+ [queryCompletion fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+/**
+ * Creates test data with special characters in field names. Datastore currently prohibits mixing
+ * nested data with special characters so tests that use this data must be separate.
+ */
+- (NSDictionary<NSString *, id> *)testDottedDataNumbered:(int)number {
+ return @{
+ @"a" : [NSString stringWithFormat:@"field %d", number],
+ @"b.dot" : @(number),
+ @"c\\slash" : @(number)
+ };
+}
+
+- (void)testFieldsWithSpecialCharsCanBeWrittenWithSet {
+ NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, testData);
+}
+
+- (void)testFieldsWithSpecialCharsCanBeReadDirectly {
+ NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result[@"a"], testData[@"a"]);
+ XCTAssertEqualObjects(result[[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]],
+ testData[@"b.dot"]);
+ XCTAssertEqualObjects(result[@"c\\slash"], testData[@"c\\slash"]);
+}
+
+- (void)testFieldsWithSpecialCharsCanBeUpdated {
+ NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+ [self updateDocumentRef:doc
+ data:@{
+ [[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] : @100,
+ @"c\\slash" : @200
+ }];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, (@{ @"a" : @"field 1", @"b.dot" : @100, @"c\\slash" : @200 }));
+}
+
+- (void)testFieldsWithSpecialCharsCanBeUsedInQueryFilters {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testDottedDataNumbered:300],
+ @"2" : [self testDottedDataNumbered:100],
+ @"3" : [self testDottedDataNumbered:200]
+ };
+
+ // inequality adds implicit sort on field
+ NSArray<NSDictionary<NSString *, id> *> *expected =
+ @[ [self testDottedDataNumbered:200], [self testDottedDataNumbered:300] ];
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"];
+ FIRQuery *q = [coll queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]
+ isGreaterThanOrEqualTo:@200];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected);
+ [queryCompletion fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testFieldsWithSpecialCharsCanBeUsedInOrderBy {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testDottedDataNumbered:300],
+ @"2" : [self testDottedDataNumbered:100],
+ @"3" : [self testDottedDataNumbered:200]
+ };
+
+ NSArray<NSDictionary<NSString *, id> *> *expected = @[
+ [self testDottedDataNumbered:100], [self testDottedDataNumbered:200],
+ [self testDottedDataNumbered:300]
+ ];
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ FIRQuery *q = [coll queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]];
+ XCTestExpectation *queryDot = [self expectationWithDescription:@"query dot"];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected);
+ [queryDot fulfill];
+ }];
+ [self awaitExpectations];
+
+ XCTestExpectation *querySlash = [self expectationWithDescription:@"query slash"];
+ q = [coll queryOrderedByField:@"c\\slash"];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected);
+ [querySlash fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m
new file mode 100644
index 0000000..19771ff
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRListenerRegistrationTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRListenerRegistrationTests
+
+- (void)testCanBeRemoved {
+ FIRCollectionReference *collectionRef = [self collectionRef];
+ FIRDocumentReference *docRef = [collectionRef documentWithAutoID];
+
+ __block int callbacks = 0;
+ id<FIRListenerRegistration> one = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacks++;
+ }];
+
+ id<FIRListenerRegistration> two = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacks++;
+ }];
+
+ // Wait for initial events
+ [self waitUntil:^BOOL {
+ return callbacks == 2;
+ }];
+
+ // Trigger new events
+ [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}];
+
+ // Write events should have triggered
+ XCTAssertEqual(4, callbacks);
+
+ // No more events should occur
+ [one remove];
+ [two remove];
+
+ [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}];
+
+ // Assert no further events occurred
+ XCTAssertEqual(4, callbacks);
+}
+
+- (void)testCanBeRemovedTwice {
+ FIRCollectionReference *collectionRef = [self collectionRef];
+ FIRDocumentReference *docRef = [collectionRef documentWithAutoID];
+
+ id<FIRListenerRegistration> one = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error){
+ }];
+ id<FIRListenerRegistration> two = [docRef
+ addSnapshotListener:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error){
+ }];
+
+ [one remove];
+ [one remove];
+
+ [two remove];
+ [two remove];
+}
+
+- (void)testCanBeRemovedIndependently {
+ FIRCollectionReference *collectionRef = [self collectionRef];
+ FIRDocumentReference *docRef = [collectionRef documentWithAutoID];
+
+ __block int callbacksOne = 0;
+ __block int callbacksTwo = 0;
+ id<FIRListenerRegistration> one = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacksOne++;
+ }];
+
+ id<FIRListenerRegistration> two = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacksTwo++;
+ }];
+
+ // Wait for initial events
+ [self waitUntil:^BOOL {
+ return callbacksOne == 1 && callbacksTwo == 1;
+ }];
+
+ // Trigger new events
+ [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}];
+
+ // Write events should have triggered
+ XCTAssertEqual(2, callbacksOne);
+ XCTAssertEqual(2, callbacksTwo);
+
+ // Should leave "two" unaffected
+ [one remove];
+
+ [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}];
+
+ // Assert only events for "two" actually occurred
+ XCTAssertEqual(2, callbacksOne);
+ XCTAssertEqual(3, callbacksTwo);
+
+ [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}];
+
+ // No more events should occur
+ [two remove];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.m b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m
new file mode 100644
index 0000000..f08df33
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTFirestoreClient.h"
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRQueryTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRQueryTests
+
+- (void)testLimitQueries {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"k" : @"a"},
+ @"b" : @{@"k" : @"b"},
+ @"c" : @{@"k" : @"c"}
+
+ }];
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[collRef queryLimitedTo:2]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"k" : @"a"}, @{@"k" : @"b"} ]));
+}
+
+- (void)testLimitQueriesWithDescendingSortOrder {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"k" : @"a", @"sort" : @0},
+ @"b" : @{@"k" : @"b", @"sort" : @1},
+ @"c" : @{@"k" : @"c", @"sort" : @1},
+ @"d" : @{@"k" : @"d", @"sort" : @2},
+
+ }];
+ FIRQuerySnapshot *snapshot =
+ [self readDocumentSetForRef:[[collRef queryOrderedByField:@"sort" descending:YES]
+ queryLimitedTo:2]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
+ @{ @"k" : @"d",
+ @"sort" : @2 },
+ @{ @"k" : @"c",
+ @"sort" : @1 }
+ ]));
+}
+
+- (void)testKeyOrderIsDescendingForDescendingInequality {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"foo" : @42},
+ @"b" : @{@"foo" : @42.0},
+ @"c" : @{@"foo" : @42},
+ @"d" : @{@"foo" : @21},
+ @"e" : @{@"foo" : @21.0},
+ @"f" : @{@"foo" : @66},
+ @"g" : @{@"foo" : @66.0},
+ }];
+ FIRQuerySnapshot *snapshot =
+ [self readDocumentSetForRef:[[collRef queryWhereField:@"foo" isGreaterThan:@21]
+ queryOrderedByField:@"foo"
+ descending:YES]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"g", @"f", @"c", @"b", @"a" ]));
+}
+
+- (void)testUnaryFilterQueries {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"null" : [NSNull null], @"nan" : @(NAN)},
+ @"b" : @{@"null" : [NSNull null], @"nan" : @0},
+ @"c" : @{@"null" : @NO, @"nan" : @(NAN)}
+ }];
+
+ FIRQuerySnapshot *results =
+ [self readDocumentSetForRef:[[collRef queryWhereField:@"null" isEqualTo:[NSNull null]]
+ queryWhereField:@"nan"
+ isEqualTo:@(NAN)]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[
+ @{ @"null" : [NSNull null],
+ @"nan" : @(NAN) }
+ ]));
+}
+
+- (void)testQueryWithFieldPaths {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"a" : @1},
+ @"b" : @{@"a" : @2},
+ @"c" : @{@"a" : @3}
+ }];
+
+ FIRQuery *query =
+ [collRef queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] isLessThan:@3];
+ query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]]
+ descending:YES];
+
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ]));
+}
+
+- (void)testFilterOnInfinity {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"inf" : @(INFINITY)},
+ @"b" : @{@"inf" : @(-INFINITY)}
+ }];
+
+ FIRQuerySnapshot *results =
+ [self readDocumentSetForRef:[collRef queryWhereField:@"inf" isEqualTo:@(INFINITY)]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ @{ @"inf" : @(INFINITY) } ]));
+}
+
+- (void)testCanExplicitlySortByDocumentID {
+ NSDictionary *testDocs = @{
+ @"a" : @{@"key" : @"a"},
+ @"b" : @{@"key" : @"b"},
+ @"c" : @{@"key" : @"c"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+
+ // Ideally this would be descending to validate it's different than
+ // the default, but that requires an extra index
+ FIRQuerySnapshot *docs =
+ [self readDocumentSetForRef:[collection queryOrderedByFieldPath:[FIRFieldPath documentID]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs),
+ (@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"c"] ]));
+}
+
+- (void)testCanQueryByDocumentID {
+ NSDictionary *testDocs = @{
+ @"aa" : @{@"key" : @"aa"},
+ @"ab" : @{@"key" : @"ab"},
+ @"ba" : @{@"key" : @"ba"},
+ @"bb" : @{@"key" : @"bb"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *docs =
+ [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isEqualTo:@"ab"]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ]));
+}
+
+- (void)testCanQueryByDocumentIDs {
+ NSDictionary *testDocs = @{
+ @"aa" : @{@"key" : @"aa"},
+ @"ab" : @{@"key" : @"ab"},
+ @"ba" : @{@"key" : @"ba"},
+ @"bb" : @{@"key" : @"bb"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *docs =
+ [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isEqualTo:@"ab"]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ]));
+
+ docs = [self readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isGreaterThan:@"aa"]
+ queryWhereFieldPath:[FIRFieldPath documentID]
+ isLessThanOrEqualTo:@"ba"]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ]));
+}
+
+- (void)testCanQueryByDocumentIDsUsingRefs {
+ NSDictionary *testDocs = @{
+ @"aa" : @{@"key" : @"aa"},
+ @"ab" : @{@"key" : @"ab"},
+ @"ba" : @{@"key" : @"ba"},
+ @"bb" : @{@"key" : @"bb"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *docs = [self
+ readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isEqualTo:[collection documentWithPath:@"ab"]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ]));
+
+ docs = [self
+ readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isGreaterThan:[collection documentWithPath:@"aa"]]
+ queryWhereFieldPath:[FIRFieldPath documentID]
+ isLessThanOrEqualTo:[collection documentWithPath:@"ba"]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ]));
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m
new file mode 100644
index 0000000..1d77e16
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTFirestoreClient.h"
+
+#import "FSTEventAccumulator.h"
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRServerTimestampTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRServerTimestampTests {
+ // Data written in tests via set.
+ NSDictionary *_setData;
+
+ // Base and update data used for update tests.
+ NSDictionary *_initialData;
+ NSDictionary *_updateData;
+
+ // A document reference to read and write to.
+ FIRDocumentReference *_docRef;
+
+ // Accumulator used to capture events during the test.
+ FSTEventAccumulator *_accumulator;
+
+ // Listener registration for a listener maintained during the course of the test.
+ id<FIRListenerRegistration> _listenerRegistration;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ // Data written in tests via set.
+ _setData = @{
+ @"a" : @42,
+ @"when" : [FIRFieldValue fieldValueForServerTimestamp],
+ @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]}
+ };
+
+ // Base and update data used for update tests.
+ _initialData = @{ @"a" : @42 };
+ _updateData = @{
+ @"when" : [FIRFieldValue fieldValueForServerTimestamp],
+ @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]}
+ };
+
+ _docRef = [self documentRef];
+ _accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ _listenerRegistration = [_docRef addSnapshotListener:_accumulator.handler];
+
+ // Wait for initial nil snapshot to avoid potential races.
+ FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"];
+ XCTAssertFalse(initialSnapshot.exists);
+}
+
+- (void)tearDown {
+ [_listenerRegistration remove];
+
+ [super tearDown];
+}
+
+// Returns the expected data, with an arbitrary timestamp substituted in.
+- (NSDictionary *)expectedDataWithTimestamp:(id _Nullable)timestamp {
+ return @{ @"a" : @42, @"when" : timestamp, @"deep" : @{@"when" : timestamp} };
+}
+
+/** Writes _initialData and waits for the corresponding snapshot. */
+- (void)writeInitialData {
+ [self writeDocumentRef:_docRef data:_initialData];
+ FIRDocumentSnapshot *initialDataSnap = [_accumulator awaitEventWithName:@"Initial data event."];
+ XCTAssertEqualObjects(initialDataSnap.data, _initialData);
+}
+
+/** Waits for a snapshot containing _setData but with NSNull for the timestamps. */
+- (void)waitForLocalEvent {
+ FIRDocumentSnapshot *localSnap = [_accumulator awaitEventWithName:@"Local event."];
+ XCTAssertEqualObjects(localSnap.data, [self expectedDataWithTimestamp:[NSNull null]]);
+}
+
+/** Waits for a snapshot containing _setData but with resolved server timestamps. */
+- (void)waitForRemoteEvent {
+ // server event should have a resolved timestamp; verify it.
+ FIRDocumentSnapshot *remoteSnap = [_accumulator awaitEventWithName:@"Remote event"];
+ XCTAssertTrue(remoteSnap.exists);
+ NSDate *when = remoteSnap[@"when"];
+ XCTAssertTrue([when isKindOfClass:[NSDate class]]);
+ // Tolerate up to 10 seconds of clock skew between client and server.
+ XCTAssertEqualWithAccuracy(when.timeIntervalSinceNow, 0, 10);
+
+ // Validate the rest of the document.
+ XCTAssertEqualObjects(remoteSnap.data, [self expectedDataWithTimestamp:when]);
+}
+
+- (void)runTransactionBlock:(void (^)(FIRTransaction *transaction))transactionBlock {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"];
+ [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ transactionBlock(transaction);
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testServerTimestampsWorkViaSet {
+ [self writeDocumentRef:_docRef data:_setData];
+ [self waitForLocalEvent];
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsWorkViaUpdate {
+ [self writeInitialData];
+ [self updateDocumentRef:_docRef data:_updateData];
+ [self waitForLocalEvent];
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsWorkViaTransactionSet {
+ [self runTransactionBlock:^(FIRTransaction *transaction) {
+ [transaction setData:_setData forDocument:_docRef];
+ }];
+
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsWorkViaTransactionUpdate {
+ [self writeInitialData];
+ [self runTransactionBlock:^(FIRTransaction *transaction) {
+ [transaction updateData:_updateData forDocument:_docRef];
+ }];
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsFailViaUpdateOnNonexistentDocument {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"update complete"];
+ [_docRef updateData:_updateData
+ completion:^(NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testServerTimestampsFailViaTransactionUpdateOnNonexistentDocument {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"];
+ [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ [transaction updateData:_updateData forDocument:_docRef];
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ // TODO(b/35201829): This should be NotFound, but right now we retry transactions on any
+ // error and so this turns into Aborted instead.
+ // TODO(mikelehen): Actually it's FailedPrecondition, unlike Android. What do we want???
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRTypeTests.m b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m
new file mode 100644
index 0000000..f3021dd
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRTypeTests.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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRTypeTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRTypeTests
+
+- (void)assertSuccessfulRoundtrip:(NSDictionary *)data {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+
+ [self writeDocumentRef:doc data:data];
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertTrue(document.exists);
+ XCTAssertEqualObjects(document.data, data);
+}
+
+- (void)testCanReadAndWriteNullFields {
+ [self assertSuccessfulRoundtrip:@{ @"a" : @1, @"b" : [NSNull null] }];
+}
+
+- (void)testCanReadAndWriteArrayFields {
+ [self assertSuccessfulRoundtrip:@{
+ @"array" : @[ @1, @"foo", @{@"deep" : @YES}, [NSNull null] ]
+ }];
+}
+
+- (void)testCanReadAndWriteBlobFields {
+ NSData *data = [NSData dataWithBytes:"\0\1\2" length:3];
+ [self assertSuccessfulRoundtrip:@{@"blob" : data}];
+}
+
+- (void)testCanReadAndWriteGeoPointFields {
+ [self assertSuccessfulRoundtrip:@{
+ @"geoPoint" : [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56]
+ }];
+}
+
+- (void)testCanReadAndWriteTimestampFields {
+ // Choose a value that can be converted losslessly between fixed point and double
+ NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:1491847082.125];
+ [self assertSuccessfulRoundtrip:@{@"timestamp" : timestamp}];
+}
+
+- (void)testCanReadAndWriteDocumentReferences {
+ // We can't use assertSuccessfulRoundtrip since FIRDocumentReference doesn't implement isEqual.
+ FIRDocumentReference *docRef = [self.db documentWithPath:@"rooms/eros"];
+ id data = @{ @"a" : @42, @"ref" : docRef };
+ [self writeDocumentRef:docRef data:data];
+
+ FIRDocumentSnapshot *readDoc = [self readDocumentForRef:docRef];
+ XCTAssertTrue(readDoc.exists);
+
+ XCTAssertEqualObjects(readDoc[@"a"], data[@"a"]);
+ FIRDocumentReference *readDocRef = readDoc[@"ref"];
+ XCTAssertTrue([readDocRef isKindOfClass:[FIRDocumentReference class]]);
+ XCTAssertEqualObjects(readDocRef.path, docRef.path);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.m b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m
new file mode 100644
index 0000000..1ba1d7a
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m
@@ -0,0 +1,560 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTHelpers.h"
+#import "FSTIntegrationTestCase.h"
+
+// We have tests for passing nil when nil is not supposed to be allowed. So suppress the warnings.
+#pragma clang diagnostic ignored "-Wnonnull"
+
+@interface FIRValidationTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRValidationTests
+
+#pragma mark - FIRFirestoreSettings Validation
+
+- (void)testNilHostFails {
+ FIRFirestoreSettings *settings = self.db.settings;
+ FSTAssertThrows(settings.host = nil,
+ @"host setting may not be nil. You should generally just use the default value "
+ "(which is firestore.googleapis.com)");
+}
+
+- (void)testNilDispatchQueueFails {
+ FIRFirestoreSettings *settings = self.db.settings;
+ FSTAssertThrows(settings.dispatchQueue = nil,
+ @"dispatch queue setting may not be nil. Create a new dispatch queue with "
+ "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default "
+ "(which is the main queue, returned from dispatch_get_main_queue())");
+}
+
+- (void)testChangingSettingsAfterUseFails {
+ FIRFirestoreSettings *settings = self.db.settings;
+ [[self.db documentWithPath:@"foo/bar"] setData:@{ @"a" : @42 }];
+ settings.host = @"example.com";
+ FSTAssertThrows(self.db.settings = settings,
+ @"Firestore instance has already been started and its settings can no longer be "
+ @"changed. You can only set settings before calling any other methods on "
+ @"a Firestore instance.");
+}
+
+#pragma mark - FIRFirestore Validation
+
+- (void)testNilFIRAppFails {
+ FSTAssertThrows(
+ [FIRFirestore firestoreForApp:nil],
+ @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd like to use the "
+ "default FirebaseApp instance.");
+}
+
+// TODO(b/62410906): Test for firestoreForApp:database: with nil DatabaseID.
+
+- (void)testNilTransactionBlocksFail {
+ FSTAssertThrows([self.db runTransactionWithBlock:nil
+ completion:^(id result, NSError *error) {
+ XCTFail(@"Completion shouldn't run.");
+ }],
+ @"Transaction block cannot be nil.");
+
+ FSTAssertThrows(
+ [self.db runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ XCTFail(@"Transaction block shouldn't run.");
+ return nil;
+ }
+ completion:nil],
+ @"Transaction completion block cannot be nil.");
+}
+
+#pragma mark - Collection and Document Path Validation
+
+- (void)testNilCollectionPathsFail {
+ FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"];
+ NSString *nilError = @"Collection path cannot be nil.";
+ FSTAssertThrows([self.db collectionWithPath:nil], nilError);
+ FSTAssertThrows([baseDocRef collectionWithPath:nil], nilError);
+}
+
+- (void)testWrongLengthCollectionPathsFail {
+ FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"];
+ NSArray *badAbsolutePaths = @[ @"foo/bar", @"foo/bar/baz/quu" ];
+ NSArray *badRelativePaths = @[ @"", @"baz/quu" ];
+ NSArray *badPathLengths = @[ @2, @4 ];
+ NSString *errorFormat =
+ @"Invalid collection reference. Collection references must have an odd "
+ @"number of segments, but %@ has %@";
+ for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) {
+ NSString *error =
+ [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]];
+ FSTAssertThrows([self.db collectionWithPath:badAbsolutePaths[i]], error);
+ FSTAssertThrows([baseDocRef collectionWithPath:badRelativePaths[i]], error);
+ }
+}
+
+- (void)testNilDocumentPathsFail {
+ FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"];
+ NSString *nilError = @"Document path cannot be nil.";
+ FSTAssertThrows([self.db documentWithPath:nil], nilError);
+ FSTAssertThrows([baseCollectionRef documentWithPath:nil], nilError);
+}
+
+- (void)testWrongLengthDocumentPathsFail {
+ FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"];
+ NSArray *badAbsolutePaths = @[ @"foo", @"foo/bar/baz" ];
+ NSArray *badRelativePaths = @[ @"", @"bar/baz" ];
+ NSArray *badPathLengths = @[ @1, @3 ];
+ NSString *errorFormat =
+ @"Invalid document reference. Document references must have an even "
+ @"number of segments, but %@ has %@";
+ for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) {
+ NSString *error =
+ [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]];
+ FSTAssertThrows([self.db documentWithPath:badAbsolutePaths[i]], error);
+ FSTAssertThrows([baseCollectionRef documentWithPath:badRelativePaths[i]], error);
+ }
+}
+
+- (void)testPathsWithEmptySegmentsFail {
+ // We're only testing using collectionWithPath since the validation happens in FSTPath which is
+ // shared by all methods that accept paths.
+
+ // leading / trailing slashes are okay.
+ [self.db collectionWithPath:@"/foo/"];
+ [self.db collectionWithPath:@"/foo"];
+ [self.db collectionWithPath:@"foo/"];
+
+ FSTAssertThrows([self.db collectionWithPath:@"foo//bar/baz"],
+ @"Invalid path (foo//bar/baz). Paths must not contain // in them.");
+ FSTAssertThrows([self.db collectionWithPath:@"//foo"],
+ @"Invalid path (//foo). Paths must not contain // in them.");
+ FSTAssertThrows([self.db collectionWithPath:@"foo//"],
+ @"Invalid path (foo//). Paths must not contain // in them.");
+}
+
+#pragma mark - Write Validation
+
+- (void)testWritesWithNonDictionaryValuesFail {
+ NSArray *badData = @[
+ @42, @"test", @[ @1 ], [NSDate date], [NSNull null], [FIRFieldValue fieldValueForDelete],
+ [FIRFieldValue fieldValueForServerTimestamp]
+ ];
+
+ for (id data in badData) {
+ [self expectWrite:data toFailWithReason:@"Data to be written must be an NSDictionary."];
+ }
+}
+
+- (void)testWritesWithNestedArraysFail {
+ [self expectWrite:@{
+ @"nested-array" : @[ @1, @[ @2 ] ]
+ }
+ toFailWithReason:@"Nested arrays are not supported"];
+}
+
+- (void)testWritesWithInvalidTypesFail {
+ [self expectWrite:@{
+ @"foo" : @{@"bar" : self}
+ }
+ toFailWithReason:@"Unsupported type: FIRValidationTests (found in field foo.bar)"];
+}
+
+- (void)testWritesWithLargeNumbersFail {
+ NSNumber *num = @((unsigned long long)LONG_MAX + 1);
+ NSString *reason =
+ [NSString stringWithFormat:@"NSNumber (%@) is too large (found in field num)", num];
+ [self expectWrite:@{@"num" : num} toFailWithReason:reason];
+}
+
+- (void)testWritesWithReferencesToADifferentDatabaseFail {
+ FIRDocumentReference *ref =
+ [[self firestoreWithProjectID:@"different-db"] documentWithPath:@"baz/quu"];
+ id data = @{@"foo" : ref};
+ [self expectWrite:data
+ toFailWithReason:
+ [NSString
+ stringWithFormat:@"Document Reference is for database different-db/(default) but "
+ "should be for database %@/(default) (found in field foo)",
+ [FSTIntegrationTestCase projectID]]];
+}
+
+- (void)testWritesWithReservedFieldsFail {
+ [self expectWrite:@{
+ @"__baz__" : @1
+ }
+ toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"];
+ [self expectWrite:@{
+ @"foo" : @{@"__baz__" : @1}
+ }
+ toFailWithReason:
+ @"Document fields cannot begin and end with __ (found in field foo.__baz__)"];
+ [self expectWrite:@{
+ @"__baz__" : @{@"foo" : @1}
+ }
+ toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"];
+
+ [self expectUpdate:@{
+ @"foo.__baz__" : @1
+ }
+ toFailWithReason:
+ @"Document fields cannot begin and end with __ (found in field foo.__baz__)"];
+ [self expectUpdate:@{
+ @"__baz__.foo" : @1
+ }
+ toFailWithReason:
+ @"Document fields cannot begin and end with __ (found in field __baz__.foo)"];
+ [self expectUpdate:@{
+ @1 : @1
+ }
+ toFailWithReason:@"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."];
+}
+
+- (void)testSetsWithFieldValueDeleteFail {
+ [self expectSet:@{@"foo" : [FIRFieldValue fieldValueForDelete]}
+ toFailWithReason:@"FieldValue.delete() can only be used with updateData()."];
+}
+
+- (void)testUpdatesWithNestedFieldValueDeleteFail {
+ [self expectUpdate:@{
+ @"foo" : @{@"bar" : [FIRFieldValue fieldValueForDelete]}
+ }
+ toFailWithReason:
+ @"FieldValue.delete() can only appear at the top level of your update data "
+ "(found in field foo.bar)"];
+}
+
+- (void)testBatchWritesWithIncorrectReferencesFail {
+ FIRFirestore *db1 = [self firestore];
+ FIRFirestore *db2 = [self firestore];
+ XCTAssertNotEqual(db1, db2);
+
+ NSString *reason = @"Provided document reference is from a different Firestore instance.";
+ id data = @{ @"foo" : @1 };
+ FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"];
+ FIRWriteBatch *batch = [db1 batch];
+ FSTAssertThrows([batch setData:data forDocument:badRef], reason);
+ FSTAssertThrows([batch setData:data forDocument:badRef options:[FIRSetOptions merge]], reason);
+ FSTAssertThrows([batch updateData:data forDocument:badRef], reason);
+ FSTAssertThrows([batch deleteDocument:badRef], reason);
+}
+
+- (void)testTransactionWritesWithIncorrectReferencesFail {
+ FIRFirestore *db1 = [self firestore];
+ FIRFirestore *db2 = [self firestore];
+ XCTAssertNotEqual(db1, db2);
+
+ NSString *reason = @"Provided document reference is from a different Firestore instance.";
+ id data = @{ @"foo" : @1 };
+ FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"];
+
+ XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"];
+ [db1 runTransactionWithBlock:^id(FIRTransaction *txn, NSError **pError) {
+ FSTAssertThrows([txn getDocument:badRef error:nil], reason);
+ FSTAssertThrows([txn setData:data forDocument:badRef], reason);
+ FSTAssertThrows([txn setData:data forDocument:badRef options:[FIRSetOptions merge]], reason);
+ FSTAssertThrows([txn updateData:data forDocument:badRef], reason);
+ FSTAssertThrows([txn deleteDocument:badRef], reason);
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ // ends up being a no-op transaction.
+ XCTAssertNil(error);
+ [transactionDone fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+#pragma mark - Field Path validation
+// TODO(b/37244157): More validation for invalid field paths.
+
+- (void)testFieldPathsWithEmptySegmentsFail {
+ NSArray *badFieldPaths = @[ @"", @"foo..baz", @".foo", @"foo." ];
+
+ for (NSString *fieldPath in badFieldPaths) {
+ NSString *reason =
+ [NSString stringWithFormat:
+ @"Invalid field path (%@). Paths must not be empty, begin with "
+ @"'.', end with '.', or contain '..'",
+ fieldPath];
+ [self expectFieldPath:fieldPath toFailWithReason:reason];
+ }
+}
+
+- (void)testFieldPathsWithInvalidSegmentsFail {
+ NSArray *badFieldPaths = @[ @"foo~bar", @"foo*bar", @"foo/bar", @"foo[1", @"foo]1", @"foo[1]" ];
+
+ for (NSString *fieldPath in badFieldPaths) {
+ NSString *reason =
+ [NSString stringWithFormat:
+ @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'",
+ fieldPath];
+ [self expectFieldPath:fieldPath toFailWithReason:reason];
+ }
+}
+
+#pragma mark - Query Validation
+
+- (void)testQueryWithNonPositiveLimitFails {
+ FSTAssertThrows([[self collectionRef] queryLimitedTo:0],
+ @"Invalid Query. Query limit (0) is invalid. Limit must be positive.");
+ FSTAssertThrows([[self collectionRef] queryLimitedTo:-1],
+ @"Invalid Query. Query limit (-1) is invalid. Limit must be positive.");
+}
+
+- (void)testQueryInequalityOnNullOrNaNFails {
+ FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:nil],
+ @"Invalid Query. You can only perform equality comparisons on nil / NSNull.");
+ FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:[NSNull null]],
+ @"Invalid Query. You can only perform equality comparisons on nil / NSNull.");
+
+ FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)],
+ @"Invalid Query. You can only perform equality comparisons on NaN.");
+}
+
+- (void)testQueryCannotBeCreatedFromDocumentsMissingSortValues {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0}
+ }];
+
+ FIRQuery *query = [testCollection queryOrderedByField:@"sort"];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"f"]];
+ XCTAssertTrue(snapshot.exists);
+
+ NSString *reason =
+ @"Invalid query. You are trying to start or end a query using a document for "
+ "which the field 'sort' (used as the order by) does not exist.";
+ FSTAssertThrows([query queryStartingAtDocument:snapshot], reason);
+ FSTAssertThrows([query queryStartingAfterDocument:snapshot], reason);
+ FSTAssertThrows([query queryEndingBeforeDocument:snapshot], reason);
+ FSTAssertThrows([query queryEndingAtDocument:snapshot], reason);
+}
+
+- (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders {
+ FIRCollectionReference *testCollection = [self collectionRef];
+ FIRQuery *query = [testCollection queryOrderedByField:@"foo"];
+
+ NSString *reason =
+ @"Invalid query. You are trying to start or end a query using more values "
+ "than were specified in the order by.";
+ // More elements than order by
+ FSTAssertThrows(([query queryStartingAtValues:@[ @1, @2 ]]), reason);
+ FSTAssertThrows(([[query queryOrderedByField:@"bar"] queryStartingAtValues:@[ @1, @2, @3 ]]),
+ reason);
+}
+
+- (void)testQueryOrderedByKeyBoundMustBeAStringWithoutSlashes {
+ FIRCollectionReference *testCollection = [self collectionRef];
+ FIRQuery *query = [testCollection queryOrderedByFieldPath:[FIRFieldPath documentID]];
+ FSTAssertThrows([query queryStartingAtValues:@[ @1 ]],
+ @"Invalid query. Expected a string for the document ID.");
+ FSTAssertThrows([query queryStartingAtValues:@[ @"foo/bar" ]],
+ @"Invalid query. Document ID 'foo/bar' contains a slash.");
+}
+
+- (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder {
+ FIRCollectionReference *testCollection = [self collectionRef];
+ FIRQuery *query = [testCollection queryOrderedByField:@"foo"];
+ NSString *reason =
+ @"Invalid query. You must not specify a starting point before specifying the order by.";
+ FSTAssertThrows([[query queryStartingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+ FSTAssertThrows([[query queryStartingAfterValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+ reason = @"Invalid query. You must not specify an ending point before specifying the order by.";
+ FSTAssertThrows([[query queryEndingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+ FSTAssertThrows([[query queryEndingBeforeValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+}
+
+- (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences {
+ FIRCollectionReference *collection = [self collectionRef];
+ NSString *reason =
+ @"Invalid query. When querying by document ID you must provide a valid "
+ "document ID, but it was an empty string.";
+ FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@""], reason);
+
+ reason =
+ @"Invalid query. When querying by document ID you must provide a valid document ID, "
+ "but 'foo/bar/baz' contains a '/' character.";
+ FSTAssertThrows(
+ [collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@"foo/bar/baz"], reason);
+
+ reason =
+ @"Invalid query. When querying by document ID you must provide a valid string or "
+ "DocumentReference, but it was of type: __NSCFNumber";
+ FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason);
+}
+
+- (void)testQueryInequalityFieldMustMatchFirstOrderByField {
+ FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
+ FIRQuery *base = [coll queryWhereField:@"x" isGreaterThanOrEqualTo:@32];
+
+ FSTAssertThrows([base queryWhereField:@"y" isLessThan:@"cat"],
+ @"Invalid Query. All where filters with an inequality (lessThan, "
+ "lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same "
+ "field. But you have inequality filters on 'x' and 'y'");
+
+ NSString *reason =
+ @"Invalid query. You have a where filter with "
+ "an inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) "
+ "on field 'x' and so you must also use 'x' as your first queryOrderedBy field, "
+ "but your first queryOrderedBy is currently on field 'y' instead.";
+ FSTAssertThrows([base queryOrderedByField:@"y"], reason);
+ FSTAssertThrows([[coll queryOrderedByField:@"y"] queryWhereField:@"x" isGreaterThan:@32], reason);
+ FSTAssertThrows([[base queryOrderedByField:@"y"] queryOrderedByField:@"x"], reason);
+ FSTAssertThrows([[[coll queryOrderedByField:@"y"] queryOrderedByField:@"x"] queryWhereField:@"x"
+ isGreaterThan:@32],
+ reason);
+
+ XCTAssertNoThrow([base queryWhereField:@"x" isLessThanOrEqualTo:@"cat"],
+ @"Same inequality fields work");
+
+ XCTAssertNoThrow([base queryWhereField:@"y" isEqualTo:@"cat"],
+ @"Inequality and equality on different fields works");
+
+ XCTAssertNoThrow([base queryOrderedByField:@"x"], @"inequality same as order by works");
+ XCTAssertNoThrow([[coll queryOrderedByField:@"x"] queryWhereField:@"x" isGreaterThan:@32],
+ @"inequality same as order by works");
+ XCTAssertNoThrow([[base queryOrderedByField:@"x"] queryOrderedByField:@"y"],
+ @"inequality same as first order by works.");
+ XCTAssertNoThrow([[[coll queryOrderedByField:@"x"] queryOrderedByField:@"y"] queryWhereField:@"x"
+ isGreaterThan:@32],
+ @"inequality same as first order by works.");
+}
+
+#pragma mark - GeoPoint Validation
+
+- (void)testInvalidGeoPointParameters {
+ [self verifyExceptionForInvalidLatitude:NAN];
+ [self verifyExceptionForInvalidLatitude:-INFINITY];
+ [self verifyExceptionForInvalidLatitude:INFINITY];
+ [self verifyExceptionForInvalidLatitude:-90.1];
+ [self verifyExceptionForInvalidLatitude:90.1];
+
+ [self verifyExceptionForInvalidLongitude:NAN];
+ [self verifyExceptionForInvalidLongitude:-INFINITY];
+ [self verifyExceptionForInvalidLongitude:INFINITY];
+ [self verifyExceptionForInvalidLongitude:-180.1];
+ [self verifyExceptionForInvalidLongitude:180.1];
+}
+
+#pragma mark - Helpers
+
+/** Performs a write using each write API and makes sure it fails with the expected reason. */
+- (void)expectWrite:(id)data toFailWithReason:(NSString *)reason {
+ [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:YES];
+}
+
+/** Performs a write using each set API and makes sure it fails with the expected reason. */
+- (void)expectSet:(id)data toFailWithReason:(NSString *)reason {
+ [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:NO];
+}
+
+/** Performs a write using each update API and makes sure it fails with the expected reason. */
+- (void)expectUpdate:(id)data toFailWithReason:(NSString *)reason {
+ [self expectWrite:data toFailWithReason:reason includeSets:NO includeUpdates:YES];
+}
+
+/**
+ * Performs a write using each set and/or update API and makes sure it fails with the expected
+ * reason.
+ */
+- (void)expectWrite:(id)data
+ toFailWithReason:(NSString *)reason
+ includeSets:(BOOL)includeSets
+ includeUpdates:(BOOL)includeUpdates {
+ FIRDocumentReference *ref = [self documentRef];
+ if (includeSets) {
+ FSTAssertThrows([ref setData:data], reason, @"for %@", data);
+ FSTAssertThrows([[ref.firestore batch] setData:data forDocument:ref], reason, @"for %@", data);
+ }
+
+ if (includeUpdates) {
+ FSTAssertThrows([ref updateData:data], reason, @"for %@", data);
+ FSTAssertThrows([[ref.firestore batch] updateData:data forDocument:ref], reason, @"for %@",
+ data);
+ }
+
+ XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"];
+ [ref.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ if (includeSets) {
+ FSTAssertThrows([transaction setData:data forDocument:ref], reason, @"for %@", data);
+ }
+ if (includeUpdates) {
+ FSTAssertThrows([transaction updateData:data forDocument:ref], reason, @"for %@", data);
+ }
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ // ends up being a no-op transaction.
+ XCTAssertNil(error);
+ [transactionDone fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testFieldNamesMustNotBeEmpty {
+ NSString *reason = @"Invalid field path. Provided names must not be empty.";
+ FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[]], reason);
+
+ reason = @"Invalid field name at index 0. Field names must not be empty.";
+ FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[ @"" ]], reason);
+
+ reason = @"Invalid field name at index 1. Field names must not be empty.";
+ FSTAssertThrows(([[FIRFieldPath alloc] initWithFields:@[ @"foo", @"" ]]), reason);
+}
+
+/**
+ * Tests a field path with all of our APIs that accept field paths and ensures they fail with the
+ * specified reason.
+ */
+- (void)expectFieldPath:(NSString *)fieldPath toFailWithReason:(NSString *)reason {
+ // Get an arbitrary snapshot we can use for testing.
+ FIRDocumentReference *docRef = [self documentRef];
+ [self writeDocumentRef:docRef data:@{ @"test" : @1 }];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:docRef];
+
+ // Update paths.
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ dict[fieldPath] = @1;
+ [self expectUpdate:dict toFailWithReason:reason];
+
+ // Snapshot fields.
+ FSTAssertThrows(snapshot[fieldPath], reason);
+
+ // Query filter / order fields.
+ FIRCollectionReference *collection = [self collectionRef];
+ FSTAssertThrows([collection queryWhereField:fieldPath isEqualTo:@1], reason);
+ // isLessThan, etc. omitted for brevity since the code path is trivially shared.
+ FSTAssertThrows([collection queryOrderedByField:fieldPath], reason);
+}
+
+- (void)verifyExceptionForInvalidLatitude:(double)latitude {
+ NSString *reason = [NSString
+ stringWithFormat:@"GeoPoint requires a latitude value in the range of [-90, 90], but was %f",
+ latitude];
+ FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:latitude longitude:0], reason);
+}
+
+- (void)verifyExceptionForInvalidLongitude:(double)longitude {
+ NSString *reason =
+ [NSString stringWithFormat:
+ @"GeoPoint requires a longitude value in the range of [-180, 180], but was %f",
+ longitude];
+ FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:0 longitude:longitude], reason);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m
new file mode 100644
index 0000000..159cbd7
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTEventAccumulator.h"
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRWriteBatchTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRWriteBatchTests
+
+- (void)testSupportEmptyBatches {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ [[[self firestore] batch] commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testSetDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{@"a" : @"b"} forDocument:doc];
+ [batch setData:@{@"c" : @"d"} forDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"});
+}
+
+- (void)testSetDocumentWithMerge {
+ FIRDocumentReference *doc = [self documentRef];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc];
+ [batch setData:@{
+ @"c" : @"d",
+ @"nested" : @{@"c" : @"d"}
+ }
+ forDocument:doc
+ options:[FIRSetOptions merge]];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(
+ snapshot.data, (
+ @{ @"a" : @"b",
+ @"c" : @"d",
+ @"nested" : @{@"a" : @"b", @"c" : @"d"} }));
+}
+
+- (void)testUpdateDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch updateData:@{ @"baz" : @42 } forDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(snapshot.data, (@{ @"foo" : @"bar", @"baz" : @42 }));
+}
+
+- (void)testCannotUpdateNonexistentDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch updateData:@{ @"baz" : @42 } forDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNotNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testDeleteDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+
+ XCTAssertTrue(snapshot.exists);
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch deleteDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ snapshot = [self readDocumentForRef:doc];
+ XCTAssertFalse(snapshot.exists);
+}
+
+- (void)testBatchesCommitAtomicallyRaisingCorrectEvents {
+ FIRCollectionReference *collection = [self collectionRef];
+ FIRDocumentReference *docA = [collection documentWithPath:@"a"];
+ FIRDocumentReference *docB = [collection documentWithPath:@"b"];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options]
+ includeQueryMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertEqual(initialSnap.count, 0);
+
+ // Atomically write two documents.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [collection.firestore batch];
+ [batch setData:@{ @"a" : @1 } forDocument:docA];
+ [batch setData:@{ @"b" : @2 } forDocument:docB];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ]));
+
+ FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ]));
+}
+
+- (void)testBatchesFailAtomicallyRaisingCorrectEvents {
+ FIRCollectionReference *collection = [self collectionRef];
+ FIRDocumentReference *docA = [collection documentWithPath:@"a"];
+ FIRDocumentReference *docB = [collection documentWithPath:@"b"];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options]
+ includeQueryMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertEqual(initialSnap.count, 0);
+
+ // Atomically write 1 document and update a nonexistent document.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch failed"];
+ FIRWriteBatch *batch = [collection.firestore batch];
+ [batch setData:@{ @"a" : @1 } forDocument:docA];
+ [batch updateData:@{ @"b" : @2 } forDocument:docB];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
+ [expectation fulfill];
+ }];
+
+ // Local event with the set document.
+ FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 } ]));
+
+ // Server event with the set reverted.
+ FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ XCTAssertEqual(serverSnap.count, 0);
+}
+
+- (void)testWriteTheSameServerTimestampAcrossWrites {
+ FIRCollectionReference *collection = [self collectionRef];
+ FIRDocumentReference *docA = [collection documentWithPath:@"a"];
+ FIRDocumentReference *docB = [collection documentWithPath:@"b"];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options]
+ includeQueryMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertEqual(initialSnap.count, 0);
+
+ // Atomically write 2 documents with server timestamps.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [collection.firestore batch];
+ [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docA];
+ [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docB];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap),
+ (@[ @{@"when" : [NSNull null]}, @{@"when" : [NSNull null]} ]));
+
+ FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ XCTAssertEqual(serverSnap.count, 2);
+ NSDate *when = serverSnap.documents[0][@"when"];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap),
+ (@[ @{@"when" : when}, @{@"when" : when} ]));
+}
+
+- (void)testCanWriteTheSameDocumentMultipleTimes {
+ FIRDocumentReference *doc = [self documentRef];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [doc
+ addSnapshotListenerWithOptions:[[FIRDocumentListenOptions options] includeMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRDocumentSnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertFalse(initialSnap.exists);
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch deleteDocument:doc];
+ [batch setData:@{ @"a" : @1, @"b" : @1, @"when" : @"when" } forDocument:doc];
+ [batch updateData:@{
+ @"b" : @2,
+ @"when" : [FIRFieldValue fieldValueForServerTimestamp]
+ }
+ forDocument:doc];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ FIRDocumentSnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(localSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : [NSNull null] }));
+
+ FIRDocumentSnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ NSDate *when = serverSnap[@"when"];
+ XCTAssertEqualObjects(serverSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : when }));
+}
+
+- (void)testUpdateFieldsWithDots {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc];
+ [batch updateData:@{
+ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"
+ }
+ forDocument:doc];
+
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"}));
+ }];
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testUpdateNestedFields {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{
+ @"a" : @{@"b" : @"old"},
+ @"c" : @{@"d" : @"old"},
+ @"e" : @{@"f" : @"old"}
+ }
+ forDocument:doc];
+ [batch updateData:@{
+ @"a.b" : @"new",
+ [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"
+ }
+ forDocument:doc];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{
+ @"a" : @{@"b" : @"new"},
+ @"c" : @{@"d" : @"new"},
+ @"e" : @{@"f" : @"old"}
+ }));
+ }];
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/CAcert.pem b/Firestore/Example/Tests/Integration/CAcert.pem
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/CAcert.pem
diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.m b/Firestore/Example/Tests/Integration/FSTDatastoreTests.m
new file mode 100644
index 0000000..bab8f44
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.m
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <GRPCClient/GRPCCall+ChannelCredentials.h>
+#import <GRPCClient/GRPCCall+Tests.h>
+#import <XCTest/XCTest.h>
+
+#import "API/FIRDocumentReference+Internal.h"
+#import "API/FSTUserDataConverter.h"
+#import "Auth/FSTEmptyCredentialsProvider.h"
+#import "Core/FSTDatabaseInfo.h"
+#import "Core/FSTFirestoreClient.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTTimestamp.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTDatastore.h"
+#import "Remote/FSTRemoteEvent.h"
+#import "Remote/FSTRemoteStore.h"
+#import "Util/FSTAssert.h"
+#import "Util/FSTDispatchQueue.h"
+#import "Util/FSTUtil.h"
+
+#import "FSTIntegrationTestCase.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTRemoteStore (Tests)
+- (void)commitBatch:(FSTMutationBatch *)batch;
+@end
+
+#pragma mark - FSTRemoteStoreEventCapture
+
+@interface FSTRemoteStoreEventCapture : NSObject <FSTRemoteSyncer>
+
+- (instancetype)init __attribute__((unavailable("Use initWithTestCase:")));
+
+- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase NS_DESIGNATED_INITIALIZER;
+
+- (void)expectWriteEventWithDescription:(NSString *)description;
+- (void)expectListenEventWithDescription:(NSString *)description;
+
+@property(nonatomic, weak, nullable) XCTestCase *testCase;
+@property(nonatomic, strong) NSMutableArray<NSObject *> *writeEvents;
+@property(nonatomic, strong) NSMutableArray<NSObject *> *listenEvents;
+@property(nonatomic, strong) NSMutableArray<XCTestExpectation *> *writeEventExpectations;
+@property(nonatomic, strong) NSMutableArray<XCTestExpectation *> *listenEventExpectations;
+@end
+
+@implementation FSTRemoteStoreEventCapture
+
+- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase {
+ if (self = [super init]) {
+ _writeEvents = [NSMutableArray array];
+ _listenEvents = [NSMutableArray array];
+ _testCase = testCase;
+ _writeEventExpectations = [NSMutableArray array];
+ _listenEventExpectations = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (void)expectWriteEventWithDescription:(NSString *)description {
+ [self.writeEventExpectations
+ addObject:[self.testCase
+ expectationWithDescription:[NSString
+ stringWithFormat:@"write event %lu: %@",
+ (unsigned long)
+ self.writeEventExpectations
+ .count,
+ description]]];
+}
+
+- (void)expectListenEventWithDescription:(NSString *)description {
+ [self.listenEventExpectations
+ addObject:[self.testCase
+ expectationWithDescription:[NSString
+ stringWithFormat:@"listen event %lu: %@",
+ (unsigned long)
+ self.listenEventExpectations
+ .count,
+ description]]];
+}
+
+- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult {
+ [self.writeEvents addObject:batchResult];
+ XCTestExpectation *expectation = [self.writeEventExpectations objectAtIndex:0];
+ [self.writeEventExpectations removeObjectAtIndex:0];
+ [expectation fulfill];
+}
+
+- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error {
+ FSTFail(@"Not implemented");
+}
+
+- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
+ [self.listenEvents addObject:remoteEvent];
+ XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0];
+ [self.listenEventExpectations removeObjectAtIndex:0];
+ [expectation fulfill];
+}
+
+- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error {
+ FSTFail(@"Not implemented");
+}
+
+@end
+
+#pragma mark - FSTDatastoreTests
+
+@interface FSTDatastoreTests : XCTestCase
+
+@end
+
+@implementation FSTDatastoreTests {
+ FSTDispatchQueue *_testWorkerQueue;
+ FSTLocalStore *_localStore;
+ id<FSTCredentialsProvider> _credentials;
+
+ FSTDatastore *_datastore;
+ FSTRemoteStore *_remoteStore;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ NSString *projectID = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"];
+ if (!projectID) {
+ projectID = @"test-db";
+ }
+
+ FIRFirestoreSettings *settings = [FSTIntegrationTestCase settings];
+ if (!settings.sslEnabled) {
+ [GRPCCall useInsecureConnectionsForHost:settings.host];
+ }
+
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:projectID database:kDefaultDatabaseID];
+
+ FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"test-key"
+ host:settings.host
+ sslEnabled:settings.sslEnabled];
+
+ _testWorkerQueue = [FSTDispatchQueue
+ queueWith:dispatch_queue_create("com.google.firestore.FSTDatastoreTestsWorkerQueue",
+ DISPATCH_QUEUE_SERIAL)];
+
+ _credentials = [[FSTEmptyCredentialsProvider alloc] init];
+
+ _datastore = [FSTDatastore datastoreWithDatabase:databaseInfo
+ workerDispatchQueue:_testWorkerQueue
+ credentials:_credentials];
+
+ _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore];
+ [_remoteStore start];
+}
+
+- (void)tearDown {
+ XCTestExpectation *completion = [self expectationWithDescription:@"shutdown"];
+ [_testWorkerQueue dispatchAsync:^{
+ [_remoteStore shutdown];
+ [completion fulfill];
+ }];
+ [self awaitExpectations];
+
+ [super tearDown];
+}
+
+- (void)testCommit {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"commitWithCompletion"];
+
+ [_datastore commitMutations:@[]
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error, @"Failed to commit");
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testStreamingWrite {
+ FSTRemoteStoreEventCapture *capture = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:self];
+ [capture expectWriteEventWithDescription:@"write mutations"];
+
+ _remoteStore.syncEngine = capture;
+
+ FSTSetMutation *mutation = [self setMutation];
+ FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23
+ localWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ mutation ]];
+ [_testWorkerQueue dispatchAsync:^{
+ [_remoteStore commitBatch:batch];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)awaitExpectations {
+ [self waitForExpectationsWithTimeout:4.0
+ handler:^(NSError *_Nullable expectationError) {
+ if (expectationError) {
+ XCTFail(@"Error waiting for timeout: %@", expectationError);
+ }
+ }];
+}
+
+- (FSTSetMutation *)setMutation {
+ return [[FSTSetMutation alloc]
+ initWithKey:[FSTDocumentKey keyWithPathString:@"rooms/eros"]
+ value:[[FSTObjectValue alloc]
+ initWithDictionary:@{@"name" : [FSTStringValue stringValue:@"Eros"]}]
+ precondition:[FSTPrecondition none]];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Integration/FSTSmokeTests.m b/Firestore/Example/Tests/Integration/FSTSmokeTests.m
new file mode 100644
index 0000000..e0ad06c
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/FSTSmokeTests.m
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTEventAccumulator.h"
+#import "FSTIntegrationTestCase.h"
+
+@interface FSTSmokeTests : FSTIntegrationTestCase
+@end
+
+@implementation FSTSmokeTests
+
+- (void)testCanWriteASingleDocument {
+ FIRDocumentReference *ref = [self documentRef];
+ [self writeDocumentRef:ref data:[self chatMessage]];
+}
+
+- (void)testCanReadAWrittenDocument {
+ NSDictionary<NSString *, id> *data = [self chatMessage];
+
+ FIRDocumentReference *ref = [self documentRef];
+ [self writeDocumentRef:ref data:data];
+
+ FIRDocumentSnapshot *doc = [self readDocumentForRef:ref];
+ XCTAssertEqualObjects(doc.data, data);
+}
+
+- (void)testObservesExistingDocument {
+ [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef) {
+ NSDictionary<NSString *, id> *data = [self chatMessage];
+ [self writeDocumentRef:writerRef data:data];
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [readerRef addSnapshotListener:self.eventAccumulator.handler];
+
+ FIRDocumentSnapshot *doc = [self.eventAccumulator awaitEventWithName:@"snapshot"];
+ XCTAssertEqual([doc class], [FIRDocumentSnapshot class]);
+ XCTAssertEqualObjects(doc.data, data);
+
+ [listenerRegistration remove];
+ }];
+}
+
+- (void)testObservesNewDocument {
+ [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef) {
+ id<FIRListenerRegistration> listenerRegistration =
+ [readerRef addSnapshotListener:self.eventAccumulator.handler];
+
+ FIRDocumentSnapshot *doc1 = [self.eventAccumulator awaitEventWithName:@"null snapshot"];
+ XCTAssertFalse(doc1.exists);
+ // TODO(b/36366944): add tests for doc1.path)
+
+ NSDictionary<NSString *, id> *data = [self chatMessage];
+ [self writeDocumentRef:writerRef data:data];
+
+ FIRDocumentSnapshot *doc2 = [self.eventAccumulator awaitEventWithName:@"full snapshot"];
+ XCTAssertEqual([doc2 class], [FIRDocumentSnapshot class]);
+ XCTAssertEqualObjects(doc2.data, data);
+
+ [listenerRegistration remove];
+ }];
+}
+
+- (void)testWillFireValueEventsForEmptyCollections {
+ FIRCollectionReference *collection = [self.db collectionWithPath:@"empty-collection"];
+ id<FIRListenerRegistration> listenerRegistration =
+ [collection addSnapshotListener:self.eventAccumulator.handler];
+
+ FIRQuerySnapshot *snap = [self.eventAccumulator awaitEventWithName:@"empty query snapshot"];
+ XCTAssertEqual([snap class], [FIRQuerySnapshot class]);
+ XCTAssertEqual(snap.count, 0);
+
+ [listenerRegistration remove];
+}
+
+- (void)testGetCollectionQuery {
+ NSDictionary<NSString *, id> *testDocs = @{
+ @"1" : @{@"name" : @"Patryk", @"message" : @"Real data, yo!"},
+ @"2" : @{@"name" : @"Gil", @"message" : @"Yep!"},
+ @"3" : @{@"name" : @"Jonny", @"message" : @"Back to work!"},
+ };
+
+ FIRCollectionReference *docs = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *result = [self readDocumentSetForRef:docs];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(result),
+ (@[ testDocs[@"1"], testDocs[@"2"], testDocs[@"3"] ]));
+}
+
+// TODO(klimt): This test is disabled because we can't create compound indexes programmatically.
+- (void)xtestQueryByFieldAndUseOrderBy {
+ NSDictionary<NSString *, id> *testDocs = @{
+ @"1" : @{@"sort" : @1, @"filter" : @YES, @"key" : @"1"},
+ @"2" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"2"},
+ @"3" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"3"},
+ @"4" : @{@"sort" : @3, @"filter" : @NO, @"key" : @"4"}
+ };
+
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ FIRQuery *query =
+ [[coll queryWhereField:@"filter" isEqualTo:@YES] queryOrderedByField:@"sort" descending:YES];
+ FIRQuerySnapshot *result = [self readDocumentSetForRef:query];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(result),
+ (@[ testDocs[@"2"], testDocs[@"3"], testDocs[@"1"] ]));
+}
+
+- (NSDictionary<NSString *, id> *)chatMessage {
+ return @{@"name" : @"Patryk", @"message" : @"We are actually writing data!"};
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/FSTTransactionTests.m b/Firestore/Example/Tests/Integration/FSTTransactionTests.m
new file mode 100644
index 0000000..a0b5bbe
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/FSTTransactionTests.m
@@ -0,0 +1,541 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+#include <libkern/OSAtomic.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FSTTransactionTests : FSTIntegrationTestCase
+@end
+
+@implementation FSTTransactionTests
+
+// We currently require every document read to also be written.
+// TODO(b/34879758): Re-enable this test once we fix it.
+- (void)xtestGetDocuments {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"spaces"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{ @"foo" : @1, @"desc" : @"Stuff", @"owner" : @"Jonny" }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ // We currently require every document read to also be written.
+ // TODO(b/34879758): Fix this check once we drop that requirement.
+ XCTAssertNotNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testDeleteDocument {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(@"bar", snapshot[@"foo"]);
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ [transaction deleteDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@YES, result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ snapshot = [self readDocumentForRef:doc];
+ XCTAssertFalse(snapshot.exists);
+}
+
+- (void)testGetNonexistentDocumentThenCreate {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertFalse(snapshot.exists);
+ [transaction setData:@{@"foo" : @"bar"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@YES, result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(@"bar", snapshot[@"foo"]);
+}
+
+- (void)testGetNonexistentDocumentThenFailPatch {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertFalse(snapshot.exists);
+ [transaction updateData:@{@"foo" : @"bar"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ // TODO(dimond): This is probably the wrong error code, but it's what we use today. We
+ // should update the code once the underlying error was fixed.
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testDeleteDocumentAndPatch {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertTrue(snapshot.exists);
+ [transaction deleteDocument:doc];
+ // Since we deleted the doc, the update will fail
+ [transaction updateData:@{@"foo" : @"bar"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ // TODO(dimond): This is probably the wrong error code, but it's what we use today. We
+ // should update the code once the underlying error was fixed.
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testDeleteDocumentAndSet {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertTrue(snapshot.exists);
+ [transaction deleteDocument:doc];
+ // TODO(dimond): In theory this should work, but it's complex to make it work, so instead we
+ // just let the transaction fail and verify it's unsupported for now
+ [transaction setData:@{@"foo" : @"new-bar"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ // TODO(dimond): This is probably the wrong error code, but it's what we use today. We
+ // should update the code once the underlying error was fixed.
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testWriteDocumentTwice {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) {
+ [transaction setData:@{@"a" : @"b"} forDocument:doc];
+ [transaction setData:@{@"c" : @"d"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@YES, result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"});
+}
+
+- (void)testSetDocumentWithMerge {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ [transaction setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc];
+ [transaction setData:@{
+ @"c" : @"d",
+ @"nested" : @{@"c" : @"d"}
+ }
+ forDocument:doc
+ options:[FIRSetOptions merge]];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@YES, result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(
+ snapshot.data, (
+ @{ @"a" : @"b",
+ @"c" : @"d",
+ @"nested" : @{@"a" : @"b", @"c" : @"d"} }));
+}
+
+- (void)testCannotUpdateNonExistentDocument {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ [transaction updateData:@{@"foo" : @"bar"} forDocument:doc];
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testIncrementTransactionally {
+ // A barrier to make sure every transaction reaches the same spot.
+ dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0);
+ __block volatile int32_t started = 0;
+
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{ @"count" : @(5.0) }];
+
+ // Make 3 transactions that will all increment.
+ int total = 3;
+ for (int i = 0; i < total; i++) {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ int32_t nowStarted = OSAtomicIncrement32(&started);
+ // Once all of the transactions have read, allow the first write.
+ if (nowStarted == total) {
+ dispatch_semaphore_signal(writeBarrier);
+ }
+
+ dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER);
+ // Refill the barrier so that the other transactions and retries succeed.
+ dispatch_semaphore_signal(writeBarrier);
+
+ double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0;
+ [transaction setData:@{ @"count" : @(newCount) } forDocument:doc];
+ return @YES;
+
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+ }
+
+ [self awaitExpectations];
+ // Now all transaction should be completed, so check the result.
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]);
+}
+
+- (void)testUpdateTransactionally {
+ // A barrier to make sure every transaction reaches the same spot.
+ dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0);
+ __block volatile int32_t started = 0;
+
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{ @"count" : @(5.0), @"other" : @"yes" }];
+
+ // Make 3 transactions that will all increment.
+ int total = 3;
+ for (int i = 0; i < total; i++) {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ int32_t nowStarted = OSAtomicIncrement32(&started);
+ // Once all of the transactions have read, allow the first write.
+ if (nowStarted == total) {
+ dispatch_semaphore_signal(writeBarrier);
+ }
+
+ dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER);
+ // Refill the barrier so that the other transactions and retries succeed.
+ dispatch_semaphore_signal(writeBarrier);
+
+ double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0;
+ [transaction updateData:@{ @"count" : @(newCount) } forDocument:doc];
+ return @YES;
+
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+ }
+
+ [self awaitExpectations];
+ // Now all transaction should be completed, so check the result.
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]);
+ XCTAssertEqualObjects(@"yes", snapshot[@"other"]);
+}
+
+// We currently require every document read to also be written.
+// TODO(b/34879758): Re-enable this test once we fix it.
+- (void)xtestHandleReadingOneDocAndWritingAnother {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc1 = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+ FIRDocumentReference *doc2 = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+
+ [self writeDocumentRef:doc1 data:@{ @"count" : @(15.0) }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ // Get the first doc.
+ [transaction getDocument:doc1 error:error];
+ XCTAssertNil(*error);
+ // Do a write outside of the transaction. The first time the
+ // transaction is tried, this will bump the version, which
+ // will cause the write to doc2 to fail. The second time, it
+ // will be a no-op and not bump the version.
+ dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0);
+ [doc1 setData:@{
+ @"count" : @(1234)
+ }
+ completion:^(NSError *_Nullable error) {
+ dispatch_semaphore_signal(writeSemaphore);
+ }];
+ // We can block on it, because transactions run on a background queue.
+ dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER);
+ // Now try to update the other doc from within the transaction.
+ // This should fail once, because we read 15 earlier.
+ [transaction setData:@{ @"count" : @(16) } forDocument:doc2];
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ // We currently require every document read to also be written.
+ // TODO(b/34879758): Add this check back once we drop that.
+ // NSError *error = nil;
+ // FIRDocument *snapshot = [transaction getDocument:doc1 error:&error];
+ // XCTAssertNil(error);
+ // XCTAssertEquals(0, tries);
+ // XCTAssertEqualObjects(@(1234), snapshot[@"count"]);
+ // snapshot = [transaction getDocument:doc2 error:&error];
+ // XCTAssertNil(error);
+ // XCTAssertEqualObjects(@(16), snapshot[@"count"]);
+ XCTAssertNotNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testReadingADocTwiceWithDifferentVersions {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{ @"count" : @(15.0) }];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ // Get the doc once.
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertEqualObjects(@(15), snapshot[@"count"]);
+ // Do a write outside of the transaction.
+ dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0);
+ [doc setData:@{
+ @"count" : @(1234)
+ }
+ completion:^(NSError *_Nullable error) {
+ dispatch_semaphore_signal(writeSemaphore);
+ }];
+ // We can block on it, because transactions run on a background queue.
+ dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER);
+ // Get the doc again in the transaction with the new version.
+ snapshot = [transaction getDocument:doc error:error];
+ // The get itself will fail, because we already read an earlier version of this document.
+ // TODO(klimt): Perhaps we shouldn't fail reads for this, but should wait and fail the
+ // whole transaction? It's an edge-case anyway, as developers shouldn't be reading the same
+ // do multiple times. But they need to handle read errors anyway.
+ XCTAssertNotNil(*error);
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(@(1234.0), snapshot[@"count"]);
+}
+
+// We currently require every document read to also be written.
+// TODO(b/34879758): Add this test back once we fix that.
+- (void)xtestCannotHaveAGetWithoutMutations {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"foo"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertNil(*error);
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testSuccessWithNoTransactionOperations {
+ FIRFirestore *firestore = [self firestore];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ return @"yes";
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@"yes", result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testCancellationOnError {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+ __block volatile int32_t count = 0;
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ OSAtomicIncrement32(&count);
+ [transaction setData:@{@"foo" : @"bar"} forDocument:doc];
+ *error = [NSError errorWithDomain:NSCocoaErrorDomain code:35 userInfo:@{}];
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ XCTAssertNotNil(error);
+ XCTAssertEqual(35, error.code);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+ XCTAssertEqual(1, (int)count);
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertFalse(snapshot.exists);
+}
+
+- (void)testUpdateFieldsWithDotsTransactionally {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUpdateFieldsWithDotsTransactionally"];
+
+ [doc.firestore
+ runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ XCTAssertNil(*error);
+ [transaction setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc];
+ [transaction updateData:@{
+ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"
+ }
+ forDocument:doc];
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"}));
+ }];
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testUpdateNestedFieldsTransactionally {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUpdateNestedFieldsTransactionally"];
+
+ [doc.firestore
+ runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ XCTAssertNil(*error);
+ [transaction setData:@{
+ @"a" : @{@"b" : @"old"},
+ @"c" : @{@"d" : @"old"},
+ @"e" : @{@"f" : @"old"}
+ }
+ forDocument:doc];
+ [transaction updateData:@{
+ @"a.b" : @"new",
+ [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"
+ }
+ forDocument:doc];
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{
+ @"a" : @{@"b" : @"new"},
+ @"c" : @{@"d" : @"new"},
+ @"e" : @{@"f" : @"old"}
+ }));
+ }];
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m
new file mode 100644
index 0000000..34c0685
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTEagerGarbageCollector.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Local/FSTReferenceSet.h"
+#import "Model/FSTDocumentKey.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTEagerGarbageCollectorTests : XCTestCase
+@end
+
+@implementation FSTEagerGarbageCollectorTests
+
+- (void)testAddOrRemoveReferences {
+ FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init];
+ FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init];
+ [gc addGarbageSource:referenceSet];
+
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ [referenceSet addReferenceToKey:key forID:1];
+ FSTAssertEqualSets([gc collectGarbage], @[]);
+ XCTAssertFalse([referenceSet isEmpty]);
+
+ [referenceSet removeReferenceToKey:key forID:1];
+ FSTAssertEqualSets([gc collectGarbage], @[ key ]);
+ XCTAssertTrue([referenceSet isEmpty]);
+}
+
+- (void)testRemoveAllReferencesForID {
+ FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init];
+ FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init];
+ [gc addGarbageSource:referenceSet];
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"];
+ [referenceSet addReferenceToKey:key1 forID:1];
+ [referenceSet addReferenceToKey:key2 forID:1];
+ [referenceSet addReferenceToKey:key3 forID:2];
+ XCTAssertFalse([referenceSet isEmpty]);
+
+ [referenceSet removeReferencesForID:1];
+ FSTAssertEqualSets([gc collectGarbage], (@[ key1, key2 ]));
+ XCTAssertFalse([referenceSet isEmpty]);
+
+ [referenceSet removeReferencesForID:2];
+ FSTAssertEqualSets([gc collectGarbage], @[ key3 ]);
+ XCTAssertTrue([referenceSet isEmpty]);
+}
+
+- (void)testTwoReferenceSetsAtTheSameTime {
+ FSTReferenceSet *remoteTargets = [[FSTReferenceSet alloc] init];
+ FSTReferenceSet *localViews = [[FSTReferenceSet alloc] init];
+ FSTReferenceSet *mutations = [[FSTReferenceSet alloc] init];
+
+ FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init];
+ [gc addGarbageSource:remoteTargets];
+ [gc addGarbageSource:localViews];
+ [gc addGarbageSource:mutations];
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ [remoteTargets addReferenceToKey:key1 forID:1];
+ [localViews addReferenceToKey:key1 forID:1];
+ [mutations addReferenceToKey:key1 forID:10];
+
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ [mutations addReferenceToKey:key2 forID:10];
+
+ XCTAssertFalse([remoteTargets isEmpty]);
+ XCTAssertFalse([localViews isEmpty]);
+ XCTAssertFalse([mutations isEmpty]);
+
+ [localViews removeReferencesForID:1];
+ FSTAssertEqualSets([gc collectGarbage], @[]);
+
+ [remoteTargets removeReferencesForID:1];
+ FSTAssertEqualSets([gc collectGarbage], @[]);
+
+ [mutations removeReferenceToKey:key1 forID:10];
+ FSTAssertEqualSets([gc collectGarbage], @[ key1 ]);
+
+ [mutations removeReferenceToKey:key2 forID:10];
+ FSTAssertEqualSets([gc collectGarbage], @[ key2 ]);
+
+ XCTAssertTrue([remoteTargets isEmpty]);
+ XCTAssertTrue([localViews isEmpty]);
+ XCTAssertTrue([mutations isEmpty]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm
new file mode 100644
index 0000000..3374fcf
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm
@@ -0,0 +1,361 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLevelDBKey.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLevelDBKeyTests : XCTestCase
+@end
+
+// I can't believe I have to write this...
+bool StartsWith(const std::string &value, const std::string &prefix) {
+ return prefix.size() <= value.size() && std::equal(prefix.begin(), prefix.end(), value.begin());
+}
+
+static std::string RemoteDocKey(NSString *pathString) {
+ return [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:FSTTestDocKey(pathString)];
+}
+
+static std::string RemoteDocKeyPrefix(NSString *pathString) {
+ return [FSTLevelDBRemoteDocumentKey keyPrefixWithResourcePath:FSTTestPath(pathString)];
+}
+
+static std::string DocMutationKey(NSString *userID, NSString *key, FSTBatchID batchID) {
+ return [FSTLevelDBDocumentMutationKey keyWithUserID:userID
+ documentKey:FSTTestDocKey(key)
+ batchID:batchID];
+}
+
+static std::string TargetDocKey(FSTTargetID targetID, NSString *key) {
+ return [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:FSTTestDocKey(key)];
+}
+
+static std::string DocTargetKey(NSString *key, FSTTargetID targetID) {
+ return [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(key) targetID:targetID];
+}
+
+/**
+ * Asserts that the description for given key is equal to the expected description.
+ *
+ * @param key A StringView of a textual key
+ * @param key An NSString that [FSTLevelDBKey descriptionForKey:] is expected to produce.
+ */
+#define FSTAssertExpectedKeyDescription(key, expectedDescription) \
+ XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:(key)], (expectedDescription))
+
+#define FSTAssertKeyLessThan(left, right) \
+ do { \
+ std::string leftKey = (left); \
+ std::string rightKey = (right); \
+ XCTAssertLessThan(leftKey.compare(right), 0, @"Expected %@ to be less than %@", \
+ [FSTLevelDBKey descriptionForKey:leftKey], \
+ [FSTLevelDBKey descriptionForKey:rightKey]); \
+ } while (0)
+
+@implementation FSTLevelDBKeyTests
+
+- (void)testMutationKeyPrefixing {
+ auto tableKey = [FSTLevelDBMutationKey keyPrefix];
+ auto emptyUserKey = [FSTLevelDBMutationKey keyPrefixWithUserID:""];
+ auto fooUserKey = [FSTLevelDBMutationKey keyPrefixWithUserID:"foo"];
+
+ auto foo2Key = [FSTLevelDBMutationKey keyWithUserID:"foo" batchID:2];
+
+ XCTAssertTrue(StartsWith(emptyUserKey, tableKey));
+
+ // This is critical: prefixes of the a value don't convert into prefixes of the key.
+ XCTAssertTrue(StartsWith(fooUserKey, tableKey));
+ XCTAssertFalse(StartsWith(fooUserKey, emptyUserKey));
+
+ // However whole segments in common are prefixes.
+ XCTAssertTrue(StartsWith(foo2Key, tableKey));
+ XCTAssertTrue(StartsWith(foo2Key, fooUserKey));
+}
+
+- (void)testMutationKeyEncodeDecodeCycle {
+ FSTLevelDBMutationKey *key = [[FSTLevelDBMutationKey alloc] init];
+ std::string user("foo");
+
+ NSArray<NSNumber *> *batchIds = @[ @0, @1, @100, @(INT_MAX - 1), @(INT_MAX) ];
+ for (NSNumber *batchIDNumber in batchIds) {
+ FSTBatchID batchID = [batchIDNumber intValue];
+ auto encoded = [FSTLevelDBMutationKey keyWithUserID:user batchID:batchID];
+
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.userID, user);
+ XCTAssertEqual(key.batchID, batchID);
+ }
+}
+
+- (void)testMutationKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBMutationKey keyPrefix], @"[mutation: incomplete key]");
+
+ FSTAssertExpectedKeyDescription([FSTLevelDBMutationKey keyPrefixWithUserID:@"user1"],
+ @"[mutation: userID=user1 incomplete key]");
+
+ auto key = [FSTLevelDBMutationKey keyWithUserID:@"user1" batchID:42];
+ FSTAssertExpectedKeyDescription(key, @"[mutation: userID=user1 batchID=42]");
+
+ FSTAssertExpectedKeyDescription(key + " extra",
+ @"[mutation: userID=user1 batchID=42 invalid "
+ @"key=<hW11dGF0aW9uAAGNdXNlcjEAAYqqgCBleHRyYQ==>]");
+
+ // Truncate the key so that it's missing its terminator.
+ key.resize(key.size() - 1);
+ FSTAssertExpectedKeyDescription(key, @"[mutation: userID=user1 batchID=42 incomplete key]");
+}
+
+- (void)testDocumentMutationKeyPrefixing {
+ auto tableKey = [FSTLevelDBDocumentMutationKey keyPrefix];
+ auto emptyUserKey = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:""];
+ auto fooUserKey = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:"foo"];
+
+ FSTDocumentKey *documentKey = FSTTestDocKey(@"foo/bar");
+ auto foo2Key =
+ [FSTLevelDBDocumentMutationKey keyWithUserID:"foo" documentKey:documentKey batchID:2];
+
+ XCTAssertTrue(StartsWith(emptyUserKey, tableKey));
+
+ // While we want a key with whole segments in common be considered a prefix it's vital that
+ // partial segments in common not be prefixes.
+ XCTAssertTrue(StartsWith(fooUserKey, tableKey));
+
+ // Here even though "" is a prefix of "foo" that prefix is within a segment so keys derived from
+ // those segments cannot be prefixes of each other.
+ XCTAssertFalse(StartsWith(fooUserKey, emptyUserKey));
+ XCTAssertFalse(StartsWith(emptyUserKey, fooUserKey));
+
+ // However whole segments in common are prefixes.
+ XCTAssertTrue(StartsWith(foo2Key, tableKey));
+ XCTAssertTrue(StartsWith(foo2Key, fooUserKey));
+}
+
+- (void)testDocumentMutationKeyEncodeDecodeCycle {
+ FSTLevelDBDocumentMutationKey *key = [[FSTLevelDBDocumentMutationKey alloc] init];
+ std::string user("foo");
+
+ NSArray<FSTDocumentKey *> *documentKeys = @[ FSTTestDocKey(@"a/b"), FSTTestDocKey(@"a/b/c/d") ];
+
+ NSArray<NSNumber *> *batchIds = @[ @0, @1, @100, @(INT_MAX - 1), @(INT_MAX) ];
+ for (NSNumber *batchIDNumber in batchIds) {
+ for (FSTDocumentKey *documentKey in documentKeys) {
+ FSTBatchID batchID = [batchIDNumber intValue];
+ auto encoded = [FSTLevelDBDocumentMutationKey keyWithUserID:user
+ documentKey:documentKey
+ batchID:batchID];
+
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.userID, user);
+ XCTAssertEqualObjects(key.documentKey, documentKey);
+ XCTAssertEqual(key.batchID, batchID);
+ }
+ }
+}
+
+- (void)testDocumentMutationKeyOrdering {
+ // Different user:
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"10", @"foo/bar", 0));
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"2", @"foo/bar", 0));
+
+ // Different paths:
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/baz", 0));
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/bar2", 0));
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0),
+ DocMutationKey(@"1", @"foo/bar/suffix/key", 0));
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar/suffix/key", 0),
+ DocMutationKey(@"1", @"foo/bar2", 0));
+
+ // Different batchID:
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/bar", 1));
+}
+
+- (void)testDocumentMutationKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBDocumentMutationKey keyPrefix],
+ @"[document_mutation: incomplete key]");
+
+ FSTAssertExpectedKeyDescription([FSTLevelDBDocumentMutationKey keyPrefixWithUserID:@"user1"],
+ @"[document_mutation: userID=user1 incomplete key]");
+
+ auto key = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:@"user1"
+ resourcePath:FSTTestPath(@"foo/bar")];
+ FSTAssertExpectedKeyDescription(key,
+ @"[document_mutation: userID=user1 key=foo/bar incomplete key]");
+
+ key = [FSTLevelDBDocumentMutationKey keyWithUserID:@"user1"
+ documentKey:FSTTestDocKey(@"foo/bar")
+ batchID:42];
+ FSTAssertExpectedKeyDescription(key, @"[document_mutation: userID=user1 key=foo/bar batchID=42]");
+}
+
+- (void)testTargetGlobalKeyEncodeDecodeCycle {
+ FSTLevelDBTargetGlobalKey *key = [[FSTLevelDBTargetGlobalKey alloc] init];
+
+ auto encoded = [FSTLevelDBTargetGlobalKey key];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+}
+
+- (void)testTargetGlobalKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBTargetGlobalKey key], @"[target_global:]");
+}
+
+- (void)testTargetKeyEncodeDecodeCycle {
+ FSTLevelDBTargetKey *key = [[FSTLevelDBTargetKey alloc] init];
+ FSTTargetID targetID = 42;
+
+ auto encoded = [FSTLevelDBTargetKey keyWithTargetID:42];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.targetID, targetID);
+}
+
+- (void)testTargetKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBTargetKey keyWithTargetID:42],
+ @"[target: targetID=42]");
+}
+
+- (void)testQueryTargetKeyEncodeDecodeCycle {
+ FSTLevelDBQueryTargetKey *key = [[FSTLevelDBQueryTargetKey alloc] init];
+ std::string canonicalID("foo");
+ FSTTargetID targetID = 42;
+
+ auto encoded = [FSTLevelDBQueryTargetKey keyWithCanonicalID:canonicalID targetID:42];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.canonicalID, canonicalID);
+ XCTAssertEqual(key.targetID, targetID);
+}
+
+- (void)testQueryKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBQueryTargetKey keyWithCanonicalID:"foo" targetID:42],
+ @"[query_target: canonicalID=foo targetID=42]");
+}
+
+- (void)testTargetDocumentKeyEncodeDecodeCycle {
+ FSTLevelDBTargetDocumentKey *key = [[FSTLevelDBTargetDocumentKey alloc] init];
+
+ auto encoded =
+ [FSTLevelDBTargetDocumentKey keyWithTargetID:42 documentKey:FSTTestDocKey(@"foo/bar")];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.targetID, 42);
+ XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(@"foo/bar"));
+}
+
+- (void)testTargetDocumentKeyOrdering {
+ // Different targetID:
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(2, @"foo/bar"));
+ FSTAssertKeyLessThan(TargetDocKey(2, @"foo/bar"), TargetDocKey(10, @"foo/bar"));
+ FSTAssertKeyLessThan(TargetDocKey(10, @"foo/bar"), TargetDocKey(100, @"foo/bar"));
+ FSTAssertKeyLessThan(TargetDocKey(42, @"foo/bar"), TargetDocKey(100, @"foo/bar"));
+
+ // Different paths:
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/baz"));
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/bar2"));
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/bar/suffix/key"));
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar/suffix/key"), TargetDocKey(1, @"foo/bar2"));
+}
+
+- (void)testTargetDocumentKeyDescription {
+ auto key = [FSTLevelDBTargetDocumentKey keyWithTargetID:42 documentKey:FSTTestDocKey(@"foo/bar")];
+ XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:key],
+ @"[target_document: targetID=42 key=foo/bar]");
+}
+
+- (void)testDocumentTargetKeyEncodeDecodeCycle {
+ FSTLevelDBDocumentTargetKey *key = [[FSTLevelDBDocumentTargetKey alloc] init];
+
+ auto encoded =
+ [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar") targetID:42];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(@"foo/bar"));
+ XCTAssertEqual(key.targetID, 42);
+}
+
+- (void)testDocumentTargetKeyDescription {
+ auto key = [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar") targetID:42];
+ XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:key],
+ @"[document_target: key=foo/bar targetID=42]");
+}
+
+- (void)testDocumentTargetKeyOrdering {
+ // Different paths:
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/baz", 1));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar2", 1));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar/suffix/key", 1));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar/suffix/key", 1), DocTargetKey(@"foo/bar2", 1));
+
+ // Different targetID:
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar", 2));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 2), DocTargetKey(@"foo/bar", 10));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 10), DocTargetKey(@"foo/bar", 100));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 42), DocTargetKey(@"foo/bar", 100));
+}
+
+- (void)testRemoteDocumentKeyPrefixing {
+ auto tableKey = [FSTLevelDBRemoteDocumentKey keyPrefix];
+
+ XCTAssertTrue(StartsWith(RemoteDocKey(@"foo/bar"), tableKey));
+
+ // This is critical: foo/bar2 should not contain foo/bar.
+ XCTAssertFalse(StartsWith(RemoteDocKey(@"foo/bar2"), RemoteDocKey(@"foo/bar")));
+
+ // Prefixes must be encoded specially
+ XCTAssertFalse(StartsWith(RemoteDocKey(@"foo/bar/baz/quu"), RemoteDocKey(@"foo/bar")));
+ XCTAssertTrue(StartsWith(RemoteDocKey(@"foo/bar/baz/quu"), RemoteDocKeyPrefix(@"foo/bar")));
+ XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar/baz/quu"), RemoteDocKeyPrefix(@"foo/bar")));
+ XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar/baz"), RemoteDocKeyPrefix(@"foo/bar")));
+ XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar"), RemoteDocKeyPrefix(@"foo")));
+}
+
+- (void)testRemoteDocumentKeyOrdering {
+ FSTAssertKeyLessThan(RemoteDocKey(@"foo/bar"), RemoteDocKey(@"foo/bar2"));
+ FSTAssertKeyLessThan(RemoteDocKey(@"foo/bar"), RemoteDocKey(@"foo/bar/suffix/key"));
+}
+
+- (void)testRemoteDocumentKeyEncodeDecodeCycle {
+ FSTLevelDBRemoteDocumentKey *key = [[FSTLevelDBRemoteDocumentKey alloc] init];
+
+ NSArray<NSString *> *paths = @[ @"foo/bar", @"foo/bar2", @"foo/bar/baz/quux" ];
+ for (NSString *path in paths) {
+ auto encoded = RemoteDocKey(path);
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(path));
+ }
+}
+
+- (void)testRemoteDocumentKeyDescription {
+ FSTAssertExpectedKeyDescription(
+ [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar/baz/quux")],
+ @"[remote_document: key=foo/bar/baz/quux]");
+}
+
+@end
+
+#undef FSTAssertExpectedKeyDescription
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m
new file mode 100644
index 0000000..d426149
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m
@@ -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 "Local/FSTLocalStore.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Auth/FSTUser.h"
+#import "Local/FSTLevelDB.h"
+
+#import "FSTLocalStoreTests.h"
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The tests for FSTLevelDBLocalStore are performed on the FSTLocalStore protocol in
+ * FSTLocalStoreTests. This class is merely responsible for creating a new FSTPersistence
+ * implementation on demand.
+ */
+@interface FSTLevelDBLocalStoreTests : FSTLocalStoreTests
+@end
+
+@implementation FSTLevelDBLocalStoreTests
+
+- (id<FSTPersistence>)persistence {
+ return [FSTPersistenceTestHelpers levelDBPersistence];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm
new file mode 100644
index 0000000..1e66aac
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm
@@ -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 "Local/FSTLevelDBMutationQueue.h"
+
+#import <XCTest/XCTest.h>
+#include <leveldb/db.h>
+
+#import "Protos/objc/firestore/local/Mutation.pbobjc.h"
+#import "Auth/FSTUser.h"
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTLevelDBKey.h"
+#import "Local/FSTWriteGroup.h"
+#include "Port/ordered_code.h"
+
+#import "FSTMutationQueueTests.h"
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+using leveldb::DB;
+using leveldb::Slice;
+using leveldb::Status;
+using leveldb::WriteOptions;
+using Firestore::StringView;
+using Firestore::OrderedCode;
+
+// A dummy mutation value, useful for testing code that's known to examine only mutation keys.
+static const char *kDummy = "1";
+
+/**
+ * Most of the tests for FSTLevelDBMutationQueue are performed on the FSTMutationQueue protocol in
+ * FSTMutationQueueTests. This class is responsible for setting up the @a mutationQueue plus any
+ * additional LevelDB-specific tests.
+ */
+@interface FSTLevelDBMutationQueueTests : FSTMutationQueueTests
+@end
+
+/**
+ * Creates a key that's structurally the same as FSTLevelDBMutationKey except it allows for
+ * nonstandard table names.
+ */
+std::string MutationLikeKey(StringView table, StringView userID, FSTBatchID batchID) {
+ std::string key;
+ OrderedCode::WriteString(&key, table);
+ OrderedCode::WriteString(&key, userID);
+ OrderedCode::WriteSignedNumIncreasing(&key, batchID);
+ return key;
+}
+
+@implementation FSTLevelDBMutationQueueTests {
+ FSTLevelDB *_db;
+}
+
+- (void)setUp {
+ [super setUp];
+ _db = [FSTPersistenceTestHelpers levelDBPersistence];
+ self.mutationQueue = [_db mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]];
+ self.persistence = _db;
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"];
+ [self.mutationQueue startWithGroup:group];
+ [self.persistence commitGroup:group];
+}
+
+- (void)testLoadNextBatchID_zeroWhenTotallyEmpty {
+ // Initial seek is invalid
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0);
+}
+
+- (void)testLoadNextBatchID_zeroWhenNoMutations {
+ // Initial seek finds no mutations
+ [self setDummyValueForKey:MutationLikeKey("mutationr", "foo", 20)];
+ [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)];
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0);
+}
+
+- (void)testLoadNextBatchID_findsSingleRow {
+ // Seeks off the end of the table altogether
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]];
+
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7);
+}
+
+- (void)testLoadNextBatchID_findsSingleRowAmongNonMutations {
+ // Seeks into table following mutations.
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]];
+ [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)];
+
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7);
+}
+
+- (void)testLoadNextBatchID_findsMaxAcrossUsers {
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"fo" batchID:5]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"food" batchID:3]];
+
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:2]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:1]];
+
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7);
+}
+
+- (void)testLoadNextBatchID_onlyFindsMutations {
+ // Write higher-valued batchIDs in nearby "tables"
+ auto tables = @[ @"mutatio", @"mutationsa", @"bears", @"zombies" ];
+ FSTBatchID highBatchID = 5;
+ for (NSString *table in tables) {
+ [self setDummyValueForKey:MutationLikeKey(table, "", highBatchID++)];
+ }
+
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"bar" batchID:3]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"bar" batchID:2]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:1]];
+
+ // None of the higher tables should match -- this is the only entry that's in the mutations
+ // table
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 4);
+}
+
+- (void)testEmptyProtoCanBeUpgraded {
+ // An empty protocol buffer serializes to a zero-length byte buffer.
+ GPBEmpty *empty = [GPBEmpty message];
+ NSData *emptyData = [empty data];
+ XCTAssertEqual(emptyData.length, 0);
+
+ // Choose some other (arbitrary) proto and parse it from the empty message and it should all be
+ // defaults. This shows that empty proto values within the index row value don't pose any future
+ // liability.
+ NSError *error;
+ FSTPBMutationQueue *parsedMessage = [FSTPBMutationQueue parseFromData:emptyData error:&error];
+ XCTAssertNil(error);
+
+ FSTPBMutationQueue *defaultMessage = [FSTPBMutationQueue message];
+ XCTAssertEqual(parsedMessage.lastAcknowledgedBatchId, defaultMessage.lastAcknowledgedBatchId);
+ XCTAssertEqualObjects(parsedMessage.lastStreamToken, defaultMessage.lastStreamToken);
+}
+
+- (void)setDummyValueForKey:(const std::string &)key {
+ _db.ptr->Put(WriteOptions(), key, kDummy);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m
new file mode 100644
index 0000000..8149397
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.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 "Local/FSTLevelDBQueryCache.h"
+
+#import "Local/FSTLevelDB.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTQueryCacheTests.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLevelDBQueryCacheTests : FSTQueryCacheTests
+@end
+
+/**
+ * The tests for FSTLevelDBQueryCache are performed on the FSTQueryCache protocol in
+ * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the
+ * @a queryCache.
+ */
+@implementation FSTLevelDBQueryCacheTests
+
+- (void)setUp {
+ [super setUp];
+
+ self.persistence = [FSTPersistenceTestHelpers levelDBPersistence];
+ self.queryCache = [self.persistence queryCache];
+ [self.queryCache start];
+}
+
+- (void)tearDown {
+ [self.queryCache shutdown];
+ self.persistence = nil;
+ self.queryCache = nil;
+
+ [super tearDown];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm
new file mode 100644
index 0000000..a6af103
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm
@@ -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 "FSTRemoteDocumentCacheTests.h"
+
+#include <leveldb/db.h>
+
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTLevelDBKey.h"
+#include "Port/ordered_code.h"
+
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+using leveldb::WriteOptions;
+using Firestore::OrderedCode;
+
+// A dummy document value, useful for testing code that's known to examine only document keys.
+static const char *kDummy = "1";
+
+/**
+ * The tests for FSTLevelDBRemoteDocumentCache are performed on the FSTRemoteDocumentCache
+ * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and
+ * tearing down the @a remoteDocumentCache.
+ */
+@interface FSTLevelDBRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests
+@end
+
+@implementation FSTLevelDBRemoteDocumentCacheTests {
+ FSTLevelDB *_db;
+}
+
+- (void)setUp {
+ [super setUp];
+ _db = [FSTPersistenceTestHelpers levelDBPersistence];
+ self.persistence = _db;
+ self.remoteDocumentCache = [self.persistence remoteDocumentCache];
+
+ // Write a couple dummy rows that should appear before/after the remote_documents table to make
+ // sure the tests are unaffected.
+ [self writeDummyRowWithSegments:@[ @"remote_documentr", @"foo", @"bar" ]];
+ [self writeDummyRowWithSegments:@[ @"remote_documentsa", @"foo", @"bar" ]];
+}
+
+- (void)tearDown {
+ [self.remoteDocumentCache shutdown];
+ self.remoteDocumentCache = nil;
+ self.persistence = nil;
+ _db = nil;
+ [super tearDown];
+}
+
+- (void)writeDummyRowWithSegments:(NSArray<NSString *> *)segments {
+ std::string key;
+ for (NSString *segment in segments) {
+ OrderedCode::WriteString(&key, segment.UTF8String);
+ }
+
+ _db.ptr->Put(WriteOptions(), key, kDummy);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m
new file mode 100644
index 0000000..0080f89
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLocalSerializer.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTTimestamp.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+#import "Model/FSTPath.h"
+#import "Protos/objc/firestore/local/MaybeDocument.pbobjc.h"
+#import "Protos/objc/firestore/local/Mutation.pbobjc.h"
+#import "Protos/objc/firestore/local/Target.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Common.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Document.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Query.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Write.pbobjc.h"
+#import "Protos/objc/google/type/Latlng.pbobjc.h"
+#import "Remote/FSTSerializerBeta.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTSerializerBeta (Test)
+- (GCFSValue *)encodedNull;
+- (GCFSValue *)encodedBool:(BOOL)value;
+- (GCFSValue *)encodedDouble:(double)value;
+- (GCFSValue *)encodedInteger:(int64_t)value;
+- (GCFSValue *)encodedString:(NSString *)value;
+@end
+
+@interface FSTLocalSerializerTests : XCTestCase
+
+@property(nonatomic, strong) FSTLocalSerializer *serializer;
+@property(nonatomic, strong) FSTSerializerBeta *remoteSerializer;
+
+@end
+
+@implementation FSTLocalSerializerTests
+
+- (void)setUp {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ self.remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID];
+ self.serializer = [[FSTLocalSerializer alloc] initWithRemoteSerializer:self.remoteSerializer];
+}
+
+- (void)testEncodesMutationBatch {
+ FSTMutation *set = FSTTestSetMutation(@"foo/bar", @{ @"a" : @"b", @"num" : @1 });
+ FSTMutation *patch = [[FSTPatchMutation alloc]
+ initWithKey:[FSTDocumentKey keyWithPathString:@"bar/baz"]
+ fieldMask:[[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"a") ]]
+ value:FSTTestObjectValue(
+ @{ @"a" : @"b",
+ @"num" : @1 })
+ precondition:[FSTPrecondition preconditionWithExists:YES]];
+ FSTMutation *del = FSTTestDeleteMutation(@"baz/quux");
+ FSTTimestamp *writeTime = [FSTTimestamp timestamp];
+ FSTMutationBatch *model = [[FSTMutationBatch alloc] initWithBatchID:42
+ localWriteTime:writeTime
+ mutations:@[ set, patch, del ]];
+
+ GCFSWrite *setProto = [GCFSWrite message];
+ setProto.update.name = @"projects/p/databases/d/documents/foo/bar";
+ [setProto.update.fields addEntriesFromDictionary:@{
+ @"a" : [self.remoteSerializer encodedString:@"b"],
+ @"num" : [self.remoteSerializer encodedInteger:1]
+ }];
+
+ GCFSWrite *patchProto = [GCFSWrite message];
+ patchProto.update.name = @"projects/p/databases/d/documents/bar/baz";
+ [patchProto.update.fields addEntriesFromDictionary:@{
+ @"a" : [self.remoteSerializer encodedString:@"b"],
+ @"num" : [self.remoteSerializer encodedInteger:1]
+ }];
+ [patchProto.updateMask.fieldPathsArray addObjectsFromArray:@[ @"a" ]];
+ patchProto.currentDocument.exists = YES;
+
+ GCFSWrite *delProto = [GCFSWrite message];
+ delProto.delete_p = @"projects/p/databases/d/documents/baz/quux";
+
+ GPBTimestamp *writeTimeProto = [GPBTimestamp message];
+ writeTimeProto.seconds = writeTime.seconds;
+ writeTimeProto.nanos = writeTime.nanos;
+
+ FSTPBWriteBatch *batchProto = [FSTPBWriteBatch message];
+ batchProto.batchId = 42;
+ [batchProto.writesArray addObjectsFromArray:@[ setProto, patchProto, delProto ]];
+ batchProto.localWriteTime = writeTimeProto;
+
+ XCTAssertEqualObjects([self.serializer encodedMutationBatch:model], batchProto);
+ FSTMutationBatch *decoded = [self.serializer decodedMutationBatch:batchProto];
+ XCTAssertEqual(decoded.batchID, model.batchID);
+ XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime);
+ XCTAssertEqualObjects(decoded.mutations, model.mutations);
+ XCTAssertEqualObjects([decoded keys], [model keys]);
+}
+
+- (void)testEncodesDocumentAsMaybeDocument {
+ FSTDocument *doc = FSTTestDoc(@"some/path", 42, @{@"foo" : @"bar"}, NO);
+
+ FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message];
+ maybeDocProto.document = [GCFSDocument message];
+ maybeDocProto.document.name = @"projects/p/databases/d/documents/some/path";
+ [maybeDocProto.document.fields addEntriesFromDictionary:@{
+ @"foo" : [self.remoteSerializer encodedString:@"bar"],
+ }];
+ maybeDocProto.document.updateTime.seconds = 0;
+ maybeDocProto.document.updateTime.nanos = 42000;
+
+ XCTAssertEqualObjects([self.serializer encodedMaybeDocument:doc], maybeDocProto);
+ FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto];
+ XCTAssertEqualObjects(decoded, doc);
+}
+
+- (void)testEncodesDeletedDocumentAsMaybeDocument {
+ FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(@"some/path", 42);
+
+ FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message];
+ maybeDocProto.noDocument = [FSTPBNoDocument message];
+ maybeDocProto.noDocument.name = @"projects/p/databases/d/documents/some/path";
+ maybeDocProto.noDocument.readTime.seconds = 0;
+ maybeDocProto.noDocument.readTime.nanos = 42000;
+
+ XCTAssertEqualObjects([self.serializer encodedMaybeDocument:deletedDoc], maybeDocProto);
+ FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto];
+ XCTAssertEqualObjects(decoded, deletedDoc);
+}
+
+- (void)testEncodesQueryData {
+ FSTQuery *query = FSTTestQuery(@"room");
+ FSTTargetID targetID = 42;
+ FSTSnapshotVersion *version = FSTTestVersion(1039);
+ NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1039);
+
+ FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:version
+ resumeToken:resumeToken];
+
+ // Let the RPC serializer test various permutations of query serialization.
+ GCFSTarget_QueryTarget *queryTarget = [self.remoteSerializer encodedQueryTarget:query];
+
+ FSTPBTarget *expected = [FSTPBTarget message];
+ expected.targetId = targetID;
+ expected.snapshotVersion.nanos = 1039000;
+ expected.resumeToken = [resumeToken copy];
+ expected.query.parent = queryTarget.parent;
+ expected.query.structuredQuery = queryTarget.structuredQuery;
+
+ XCTAssertEqualObjects([self.serializer encodedQueryData:queryData], expected);
+ FSTQueryData *decoded = [self.serializer decodedQueryData:expected];
+ XCTAssertEqualObjects(decoded, queryData);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.h b/Firestore/Example/Tests/Local/FSTLocalStoreTests.h
new file mode 100644
index 0000000..8e06d82
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.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 <XCTest/XCTest.h>
+
+@class FSTLocalStore;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * These are tests for any implementation of the FSTLocalStore protocol.
+ *
+ * To test a specific implementation of FSTLocalStore:
+ *
+ * + Subclass FSTLocalStoreTests
+ * + override -persistence, creating a new instance of FSTPersistence.
+ */
+@interface FSTLocalStoreTests : XCTestCase
+
+/** Creates and returns an appropriate id<FSTPersistence> implementation. */
+- (id<FSTPersistence>)persistence;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m
new file mode 100644
index 0000000..ab492a7
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m
@@ -0,0 +1,795 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLocalStore.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Auth/FSTUser.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTTimestamp.h"
+#import "Local/FSTEagerGarbageCollector.h"
+#import "Local/FSTLocalWriteResult.h"
+#import "Local/FSTNoOpGarbageCollector.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTDocumentSet.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTRemoteEvent.h"
+#import "Remote/FSTWatchChange.h"
+#import "Util/FSTClasses.h"
+
+#import "FSTHelpers.h"
+#import "FSTImmutableSortedDictionary+Testing.h"
+#import "FSTImmutableSortedSet+Testing.h"
+#import "FSTLocalStoreTests.h"
+#import "FSTWatchChange+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Creates a document version dictionary mapping the document in @a mutation to @a version. */
+FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation,
+ FSTTestSnapshotVersion version) {
+ FSTDocumentVersionDictionary *result = [FSTDocumentVersionDictionary documentVersionDictionary];
+ result = [result dictionaryBySettingObject:FSTTestVersion(version) forKey:mutation.key];
+ return result;
+}
+
+@interface FSTLocalStoreTests ()
+
+@property(nonatomic, strong, readwrite) id<FSTPersistence> localStorePersistence;
+@property(nonatomic, strong, readwrite) FSTLocalStore *localStore;
+
+@property(nonatomic, strong, readonly) NSMutableArray<FSTMutationBatch *> *batches;
+@property(nonatomic, strong, readwrite, nullable) FSTMaybeDocumentDictionary *lastChanges;
+@property(nonatomic, assign, readwrite) FSTTargetID lastTargetID;
+
+@end
+
+@implementation FSTLocalStoreTests
+
+- (void)setUp {
+ [super setUp];
+
+ if ([self isTestBaseClass]) {
+ return;
+ }
+
+ id<FSTPersistence> persistence = [self persistence];
+ self.localStorePersistence = persistence;
+ id<FSTGarbageCollector> garbageCollector = [[FSTEagerGarbageCollector alloc] init];
+ self.localStore = [[FSTLocalStore alloc] initWithPersistence:persistence
+ garbageCollector:garbageCollector
+ initialUser:[FSTUser unauthenticatedUser]];
+ [self.localStore start];
+
+ _batches = [NSMutableArray array];
+ _lastChanges = nil;
+ _lastTargetID = 0;
+}
+
+- (void)tearDown {
+ [self.localStore shutdown];
+ [self.localStorePersistence shutdown];
+
+ [super tearDown];
+}
+
+- (id<FSTPersistence>)persistence {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+/**
+ * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for
+ * FSTLocalStoreTests since it is incomplete without the implementations supplied by its
+ * subclasses.
+ */
+- (BOOL)isTestBaseClass {
+ return [self class] == [FSTLocalStoreTests class];
+}
+
+/** Restarts the local store using the FSTNoOpGarbageCollector instead of the default. */
+- (void)restartWithNoopGarbageCollector {
+ [self.localStore shutdown];
+
+ id<FSTGarbageCollector> garbageCollector = [[FSTNoOpGarbageCollector alloc] init];
+ self.localStore = [[FSTLocalStore alloc] initWithPersistence:self.localStorePersistence
+ garbageCollector:garbageCollector
+ initialUser:[FSTUser unauthenticatedUser]];
+ [self.localStore start];
+}
+
+- (void)writeMutation:(FSTMutation *)mutation {
+ [self writeMutations:@[ mutation ]];
+}
+
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
+ FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations];
+ XCTAssertNotNil(result);
+ [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batchID
+ localWriteTime:[FSTTimestamp timestamp]
+ mutations:mutations]];
+ self.lastChanges = result.changes;
+}
+
+- (void)applyRemoteEvent:(FSTRemoteEvent *)event {
+ self.lastChanges = [self.localStore applyRemoteEvent:event];
+}
+
+- (void)notifyLocalViewChanges:(FSTLocalViewChanges *)changes {
+ [self.localStore notifyLocalViewChanges:@[ changes ]];
+}
+
+- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion {
+ FSTMutationBatch *batch = [self.batches firstObject];
+ [self.batches removeObjectAtIndex:0];
+ XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported.");
+ FSTSnapshotVersion *version = FSTTestVersion(documentVersion);
+ FSTMutationResult *mutationResult =
+ [[FSTMutationResult alloc] initWithVersion:version transformResults:nil];
+ FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch
+ commitVersion:version
+ mutationResults:@[ mutationResult ]
+ streamToken:nil];
+ self.lastChanges = [self.localStore acknowledgeBatchWithResult:result];
+}
+
+- (void)rejectMutation {
+ FSTMutationBatch *batch = [self.batches firstObject];
+ [self.batches removeObjectAtIndex:0];
+ self.lastChanges = [self.localStore rejectBatchID:batch.batchID];
+}
+
+- (void)allocateQuery:(FSTQuery *)query {
+ FSTQueryData *queryData = [self.localStore allocateQuery:query];
+ self.lastTargetID = queryData.targetID;
+}
+
+- (void)collectGarbage {
+ [self.localStore collectGarbage];
+}
+
+/** Asserts that the last target ID is the given number. */
+#define FSTAssertTargetID(targetID) \
+ do { \
+ XCTAssertEqual(self.lastTargetID, targetID); \
+ } while (0)
+
+/** Asserts that a the lastChanges contain the docs in the given array. */
+#define FSTAssertChanged(documents) \
+ XCTAssertNotNil(self.lastChanges); \
+ do { \
+ FSTMaybeDocumentDictionary *actual = self.lastChanges; \
+ NSArray<FSTMaybeDocument *> *expected = (documents); \
+ XCTAssertEqual(actual.count, expected.count); \
+ NSEnumerator<FSTMaybeDocument *> *enumerator = expected.objectEnumerator; \
+ [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * key, FSTMaybeDocument * value, \
+ BOOL * stop) { \
+ XCTAssertEqualObjects(value, [enumerator nextObject]); \
+ }]; \
+ self.lastChanges = nil; \
+ } while (0)
+
+/** Asserts that the given keys were removed. */
+#define FSTAssertRemoved(keyPaths) \
+ XCTAssertNotNil(self.lastChanges); \
+ do { \
+ FSTMaybeDocumentDictionary *actual = self.lastChanges; \
+ XCTAssertEqual(actual.count, keyPaths.count); \
+ NSEnumerator<NSString *> *keyPathEnumerator = keyPaths.objectEnumerator; \
+ [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * actualKey, \
+ FSTMaybeDocument * value, BOOL * stop) { \
+ FSTDocumentKey *expectedKey = \
+ [FSTDocumentKey keyWithPathString:[keyPathEnumerator nextObject]]; \
+ XCTAssertEqualObjects(actualKey, expectedKey); \
+ XCTAssertTrue([value isKindOfClass:[FSTDeletedDocument class]]); \
+ }]; \
+ self.lastChanges = nil; \
+ } while (0)
+
+/** Asserts that the given local store contains the given document. */
+#define FSTAssertContains(document) \
+ do { \
+ FSTMaybeDocument *expected = (document); \
+ FSTMaybeDocument *actual = [self.localStore readDocument:expected.key]; \
+ XCTAssertEqualObjects(actual, expected); \
+ } while (0)
+
+/** Asserts that the given local store does not contain the given document. */
+#define FSTAssertNotContains(keyPathString) \
+ do { \
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPathString]; \
+ FSTMaybeDocument *actual = [self.localStore readDocument:key]; \
+ XCTAssertNil(actual); \
+ } while (0)
+
+- (void)testMutationBatchKeys {
+ if ([self isTestBaseClass]) return;
+
+ FSTMutation *set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"});
+ FSTMutation *set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"});
+ FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1
+ localWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ set1, set2 ]];
+ FSTDocumentKeySet *keys = [batch keys];
+ XCTAssertEqual(keys.count, 2);
+}
+
+- (void)testHandlesSetMutation {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:0];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO));
+}
+
+- (void)testHandlesSetMutationThenDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
+ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES));
+}
+
+- (void)testHandlesAckThenRejectThenRemoteEvent {
+ if ([self isTestBaseClass]) return;
+
+ // Start a query that requires acks to be held.
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [self allocateQuery:query];
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ // The last seen version is zero, so this ack must be held.
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertChanged(@[]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self writeMutation:FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES));
+
+ [self rejectMutation];
+ FSTAssertRemoved(@[ @"bar/baz" ]);
+ FSTAssertNotContains(@"bar/baz");
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
+ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO));
+ FSTAssertNotContains(@"bar/baz");
+}
+
+- (void)testHandlesDeletedDocumentThenSetMutationThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2));
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:3];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO));
+}
+
+- (void)testHandlesSetMutationThenDeletedDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+}
+
+- (void)testHandlesDocumentThenSetMutationThenAckThenDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO));
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:3];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
+ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO));
+}
+
+- (void)testHandlesPatchWithoutPriorDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+}
+
+- (void)testHandlesPatchMutationThenDocumentThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES));
+
+ [self acknowledgeMutationWithVersion:2];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO));
+}
+
+- (void)testHandlesPatchMutationThenAckThenDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO));
+}
+
+- (void)testHandlesDeleteMutationThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+}
+
+- (void)testHandlesDocumentThenDeleteMutationThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO));
+
+ [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:2];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+}
+
+- (void)testHandlesDeleteMutationThenDocumentThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:2];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+}
+
+- (void)testHandlesDocumentThenDeletedDocumentThenDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
+ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO));
+}
+
+- (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES));
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:2]; // delete mutation
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:3]; // patch mutation
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO));
+}
+
+- (void)testHandlesSetMutationAndPatchMutationTogether {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutations:@[
+ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}),
+ FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)
+ ]];
+
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+}
+
+- (void)testHandlesSetMutationThenPatchMutationThenReject {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})];
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO));
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self rejectMutation];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO));
+}
+
+- (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutations:@[
+ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}),
+ FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}),
+ FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)
+ ]];
+
+ FSTAssertChanged((@[
+ FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES),
+ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)
+ ]));
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+ FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES));
+}
+
+- (void)testHandlesDeleteMutationThenPatchMutationThenAckThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:2]; // delete mutation
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:3]; // patch mutation
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+}
+
+- (void)testCollectsGarbageAfterChangeBatchWithNoTargetIDs {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO),
+ @[ @1 ], @[])];
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+}
+
+- (void)testCollectsGarbageAfterChangeBatch {
+ if ([self isTestBaseClass]) return;
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [self allocateQuery:query];
+ FSTAssertTargetID(2);
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO),
+ @[ @2 ], @[])];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"baz"}, NO),
+ @[], @[ @2 ])];
+ [self collectGarbage];
+
+ FSTAssertNotContains(@"foo/bar");
+}
+
+- (void)testCollectsGarbageAfterAcknowledgedMutation {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO),
+ @[ @1 ], @[])];
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})];
+ [self writeMutation:FSTTestDeleteMutation(@"foo/baz")];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+ FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES));
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self acknowledgeMutationWithVersion:3];
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES));
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self acknowledgeMutationWithVersion:4];
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/bah");
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self acknowledgeMutationWithVersion:5];
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/bah");
+ FSTAssertNotContains(@"foo/baz");
+}
+
+- (void)testCollectsGarbageAfterRejectedMutation {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO),
+ @[ @1 ], @[])];
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})];
+ [self writeMutation:FSTTestDeleteMutation(@"foo/baz")];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+ FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES));
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self rejectMutation]; // patch mutation
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES));
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self rejectMutation]; // set mutation
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/bah");
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self rejectMutation]; // delete mutation
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/bah");
+ FSTAssertNotContains(@"foo/baz");
+}
+
+- (void)testPinsDocumentsInTheLocalView {
+ if ([self isTestBaseClass]) return;
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [self allocateQuery:query];
+ FSTAssertTargetID(2);
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO),
+ @[ @2 ], @[])];
+ [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO));
+ FSTAssertContains(FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES));
+
+ [self notifyLocalViewChanges:FSTTestViewChanges(query, @[ @"foo/bar", @"foo/baz" ], @[])];
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO),
+ @[], @[ @2 ])];
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO),
+ @[ @1 ], @[])];
+ [self acknowledgeMutationWithVersion:2];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO));
+ FSTAssertContains(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO));
+
+ [self notifyLocalViewChanges:FSTTestViewChanges(query, @[], @[ @"foo/bar", @"foo/baz" ])];
+ [self collectGarbage];
+
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/baz");
+}
+
+- (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately {
+ if ([self isTestBaseClass]) return;
+
+ FSTTargetID targetID = 321;
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{}, NO),
+ @[ @(targetID) ], @[])];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{}, NO));
+
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+}
+
+- (void)testCanExecuteDocumentQueries {
+ if ([self isTestBaseClass]) return;
+
+ [self.localStore locallyWriteMutations:@[
+ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}),
+ FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}),
+ FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"})
+ ]];
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+ FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
+ XCTAssertEqualObjects([docs values], @[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+}
+
+- (void)testCanExecuteCollectionQueries {
+ if ([self isTestBaseClass]) return;
+
+ [self.localStore locallyWriteMutations:@[
+ FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}),
+ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}),
+ FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}),
+ FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}),
+ FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"})
+ ]];
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
+ XCTAssertEqualObjects([docs values], (@[
+ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES),
+ FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES)
+ ]));
+}
+
+- (void)testCanExecuteMixedCollectionQueries {
+ if ([self isTestBaseClass]) return;
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [self allocateQuery:query];
+ FSTAssertTargetID(2);
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO),
+ @[ @2 ], @[])];
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO),
+ @[ @2 ], @[])];
+
+ [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]];
+
+ FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
+ XCTAssertEqualObjects([docs values], (@[
+ FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO),
+ FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO),
+ FSTTestDoc(@"foo/bonk", 0, @{@"a" : @"b"}, YES)
+ ]));
+}
+
+- (void)testPersistsResumeTokens {
+ if ([self isTestBaseClass]) return;
+
+ // This test only works in the absence of the FSTEagerGarbageCollector.
+ [self restartWithNoopGarbageCollector];
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+ FSTQueryData *queryData = [self.localStore allocateQuery:query];
+ FSTBoxedTargetID *targetID = @(queryData.targetID);
+ NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000);
+
+ FSTWatchChange *watchChange =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ targetID ]
+ resumeToken:resumeToken];
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *listens =
+ [NSMutableDictionary dictionary];
+ listens[targetID] = queryData;
+ NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingResponses =
+ [NSMutableDictionary dictionary];
+ FSTWatchChangeAggregator *aggregator =
+ [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(1000)
+ listenTargets:listens
+ pendingTargetResponses:pendingResponses];
+ [aggregator addWatchChanges:@[ watchChange ]];
+ FSTRemoteEvent *remoteEvent = [aggregator remoteEvent];
+ [self applyRemoteEvent:remoteEvent];
+
+ // Stop listening so that the query should become inactive (but persistent)
+ [self.localStore releaseQuery:query];
+
+ // Should come back with the same resume token
+ FSTQueryData *queryData2 = [self.localStore allocateQuery:query];
+ XCTAssertEqualObjects(queryData2.resumeToken, resumeToken);
+}
+
+- (void)testRemoteDocumentKeysForTarget {
+ if ([self isTestBaseClass]) return;
+ [self restartWithNoopGarbageCollector];
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [self allocateQuery:query];
+ FSTAssertTargetID(2);
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO),
+ @[ @2 ], @[])];
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO),
+ @[ @2 ], @[])];
+
+ [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]];
+
+ FSTDocumentKeySet *keys = [self.localStore remoteDocumentKeysForTarget:2];
+ FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ]));
+
+ [self restartWithNoopGarbageCollector];
+
+ keys = [self.localStore remoteDocumentKeysForTarget:2];
+ FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ]));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m
new file mode 100644
index 0000000..e7486d0
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m
@@ -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 "Local/FSTLocalStore.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTLocalStoreTests.h"
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * This tests the FSTLocalStore with an FSTMemoryPersistence persistence implementation. The tests
+ * are in FSTLocalStoreTests and this class is merely responsible for creating a new FSTPersistence
+ * implementation on demand.
+ */
+@interface FSTMemoryLocalStoreTests : FSTLocalStoreTests
+@end
+
+@implementation FSTMemoryLocalStoreTests
+
+- (id<FSTPersistence>)persistence {
+ return [FSTPersistenceTestHelpers memoryPersistence];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m
new file mode 100644
index 0000000..4d76393
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m
@@ -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 "Local/FSTMemoryMutationQueue.h"
+
+#import "Auth/FSTUser.h"
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTMutationQueueTests.h"
+#import "FSTPersistenceTestHelpers.h"
+
+@interface FSTMemoryMutationQueueTests : FSTMutationQueueTests
+@end
+
+/**
+ * The tests for FSTMemoryMutationQueue are performed on the FSTMutationQueue protocol in
+ * FSTMutationQueueTests. This class is merely responsible for setting up the @a mutationQueue.
+ */
+@implementation FSTMemoryMutationQueueTests
+
+- (void)setUp {
+ [super setUp];
+
+ self.persistence = [FSTPersistenceTestHelpers memoryPersistence];
+ self.mutationQueue =
+ [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m
new file mode 100644
index 0000000..6574647
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.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 "Local/FSTMemoryQueryCache.h"
+
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTQueryCacheTests.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMemoryQueryCacheTests : FSTQueryCacheTests
+@end
+
+/**
+ * The tests for FSTMemoryQueryCache are performed on the FSTQueryCache protocol in
+ * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the
+ * @a queryCache.
+ */
+@implementation FSTMemoryQueryCacheTests
+
+- (void)setUp {
+ [super setUp];
+
+ self.persistence = [FSTPersistenceTestHelpers memoryPersistence];
+ self.queryCache = [self.persistence queryCache];
+ [self.queryCache start];
+}
+
+- (void)tearDown {
+ [self.queryCache shutdown];
+ self.persistence = nil;
+ self.queryCache = nil;
+
+ [super tearDown];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m
new file mode 100644
index 0000000..7602134
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m
@@ -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 "Local/FSTMemoryRemoteDocumentCache.h"
+
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTRemoteDocumentCacheTests.h"
+
+@interface FSTMemoryRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests
+@end
+
+/**
+ * The tests for FSTMemoryRemoteDocumentCache are performed on the FSTRemoteDocumentCache
+ * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and
+ * tearing down the @a remoteDocumentCache.
+ */
+@implementation FSTMemoryRemoteDocumentCacheTests
+
+- (void)setUp {
+ [super setUp];
+
+ self.persistence = [FSTPersistenceTestHelpers memoryPersistence];
+ self.remoteDocumentCache = [self.persistence remoteDocumentCache];
+}
+
+- (void)tearDown {
+ [self.remoteDocumentCache shutdown];
+ self.persistence = nil;
+ self.remoteDocumentCache = nil;
+
+ [super tearDown];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.h b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h
new file mode 100644
index 0000000..0193c36
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.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 <XCTest/XCTest.h>
+
+@protocol FSTMutationQueue;
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * These are tests for any implementation of the FSTMutationQueue protocol.
+ *
+ * To test a specific implementation of FSTMutationQueue:
+ *
+ * + Subclass FSTMutationQueueTests
+ * + override -setUp, assigning to mutationQueue and persistence
+ * + override -tearDown, cleaning up mutationQueue and persistence
+ */
+@interface FSTMutationQueueTests : XCTestCase
+@property(nonatomic, strong, nullable) id<FSTMutationQueue> mutationQueue;
+@property(nonatomic, strong, nullable) id<FSTPersistence> persistence;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m
new file mode 100644
index 0000000..42ba0b3
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m
@@ -0,0 +1,511 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTMutationQueueTests.h"
+
+#import "Auth/FSTUser.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTTimestamp.h"
+#import "Local/FSTEagerGarbageCollector.h"
+#import "Local/FSTMutationQueue.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTWriteGroup.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTMutationQueueTests
+
+- (void)tearDown {
+ [self.mutationQueue shutdown];
+ [self.persistence shutdown];
+ [super tearDown];
+}
+
+/**
+ * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for
+ * FSTMutationQueueTests since it is incomplete without the implementations supplied by its
+ * subclasses.
+ */
+- (BOOL)isTestBaseClass {
+ return [self class] == [FSTMutationQueueTests class];
+}
+
+- (void)testCountBatches {
+ if ([self isTestBaseClass]) return;
+
+ XCTAssertEqual(0, [self batchCount]);
+ XCTAssertTrue([self.mutationQueue isEmpty]);
+
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+ XCTAssertEqual(1, [self batchCount]);
+ XCTAssertFalse([self.mutationQueue isEmpty]);
+
+ FSTMutationBatch *batch2 = [self addMutationBatch];
+ XCTAssertEqual(2, [self batchCount]);
+
+ [self removeMutationBatches:@[ batch2 ]];
+ XCTAssertEqual(1, [self batchCount]);
+
+ [self removeMutationBatches:@[ batch1 ]];
+ XCTAssertEqual(0, [self batchCount]);
+ XCTAssertTrue([self.mutationQueue isEmpty]);
+}
+
+- (void)testAcknowledgeBatchID {
+ if ([self isTestBaseClass]) return;
+
+ // Initial state of an empty queue
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown);
+
+ // Adding mutation batches should not change the highest acked batchID.
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+ FSTMutationBatch *batch2 = [self addMutationBatch];
+ FSTMutationBatch *batch3 = [self addMutationBatch];
+ XCTAssertGreaterThan(batch1.batchID, kFSTBatchIDUnknown);
+ XCTAssertGreaterThan(batch2.batchID, batch1.batchID);
+ XCTAssertGreaterThan(batch3.batchID, batch2.batchID);
+
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown);
+
+ [self acknowledgeBatch:batch1];
+ [self acknowledgeBatch:batch2];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ [self removeMutationBatches:@[ batch1 ]];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ [self removeMutationBatches:@[ batch2 ]];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ // Batch 3 never acknowledged.
+ [self removeMutationBatches:@[ batch3 ]];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+}
+
+- (void)testAcknowledgeThenRemove {
+ if ([self isTestBaseClass]) return;
+
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:NSStringFromSelector(_cmd)];
+ [self.mutationQueue acknowledgeBatch:batch1 streamToken:nil group:group];
+ [self.mutationQueue removeMutationBatches:@[ batch1 ] group:group];
+ [self.persistence commitGroup:group];
+
+ XCTAssertEqual([self batchCount], 0);
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch1.batchID);
+}
+
+- (void)testHighestAcknowledgedBatchIDNeverExceedsNextBatchID {
+ if ([self isTestBaseClass]) return;
+
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+ FSTMutationBatch *batch2 = [self addMutationBatch];
+ [self acknowledgeBatch:batch1];
+ [self acknowledgeBatch:batch2];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ [self removeMutationBatches:@[ batch1, batch2 ]];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ // Restart the queue so that nextBatchID will be reset.
+ [self.mutationQueue shutdown];
+ self.mutationQueue =
+ [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"];
+ [self.mutationQueue startWithGroup:group];
+ [self.persistence commitGroup:group];
+
+ // Verify that on restart with an empty queue, nextBatchID falls to a lower value.
+ XCTAssertLessThan(self.mutationQueue.nextBatchID, batch2.batchID);
+
+ // As a result highestAcknowledgedBatchID must also reset lower.
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown);
+
+ // The mutation queue will reset the next batchID after all mutations are removed so adding
+ // another mutation will cause a collision.
+ FSTMutationBatch *newBatch = [self addMutationBatch];
+ XCTAssertEqual(newBatch.batchID, batch1.batchID);
+
+ // Restart the queue with one unacknowledged batch in it.
+ group = [self.persistence startGroupWithAction:@"Start MutationQueue"];
+ [self.mutationQueue startWithGroup:group];
+ [self.persistence commitGroup:group];
+
+ XCTAssertEqual([self.mutationQueue nextBatchID], newBatch.batchID + 1);
+
+ // highestAcknowledgedBatchID must still be kFSTBatchIDUnknown.
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown);
+}
+
+- (void)testLookupMutationBatch {
+ if ([self isTestBaseClass]) return;
+
+ // Searching on an empty queue should not find a non-existent batch
+ FSTMutationBatch *notFound = [self.mutationQueue lookupMutationBatch:42];
+ XCTAssertNil(notFound);
+
+ NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10];
+ NSArray<FSTMutationBatch *> *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches];
+
+ // After removing, a batch should not be found
+ for (NSUInteger i = 0; i < removed.count; i++) {
+ notFound = [self.mutationQueue lookupMutationBatch:removed[i].batchID];
+ XCTAssertNil(notFound);
+ }
+
+ // Remaining entries should still be found
+ for (FSTMutationBatch *batch in batches) {
+ FSTMutationBatch *found = [self.mutationQueue lookupMutationBatch:batch.batchID];
+ XCTAssertEqual(found.batchID, batch.batchID);
+ }
+
+ // Even on a nonempty queue searching should not find a non-existent batch
+ notFound = [self.mutationQueue lookupMutationBatch:42];
+ XCTAssertNil(notFound);
+}
+
+- (void)testNextMutationBatchAfterBatchID {
+ if ([self isTestBaseClass]) return;
+
+ NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10];
+
+ // This is an array of successors assuming the removals below will happen:
+ NSArray<FSTMutationBatch *> *afters = @[ batches[3], batches[8], batches[8] ];
+ NSArray<FSTMutationBatch *> *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches];
+
+ for (NSUInteger i = 0; i < batches.count - 1; i++) {
+ FSTMutationBatch *current = batches[i];
+ FSTMutationBatch *next = batches[i + 1];
+ FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID];
+ XCTAssertEqual(found.batchID, next.batchID);
+ }
+
+ for (NSUInteger i = 0; i < removed.count; i++) {
+ FSTMutationBatch *current = removed[i];
+ FSTMutationBatch *next = afters[i];
+ FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID];
+ XCTAssertEqual(found.batchID, next.batchID);
+ }
+
+ FSTMutationBatch *first = batches[0];
+ FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:first.batchID - 42];
+ XCTAssertEqual(found.batchID, first.batchID);
+
+ FSTMutationBatch *last = batches[batches.count - 1];
+ FSTMutationBatch *notFound = [self.mutationQueue nextMutationBatchAfterBatchID:last.batchID];
+ XCTAssertNil(notFound);
+}
+
+- (void)testAllMutationBatchesThroughBatchID {
+ if ([self isTestBaseClass]) return;
+
+ NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10];
+ [self makeHoles:@[ @2, @6, @7 ] inBatches:batches];
+
+ NSArray<FSTMutationBatch *> *found, *expected;
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[0].batchID - 1];
+ XCTAssertEqualObjects(found, (@[]));
+
+ for (NSUInteger i = 0; i < batches.count; i++) {
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[i].batchID];
+ expected = [batches subarrayWithRange:NSMakeRange(0, i + 1)];
+ XCTAssertEqualObjects(found, expected, @"for index %lu", (unsigned long)i);
+ }
+}
+
+- (void)testAllMutationBatchesAffectingDocumentKey {
+ if ([self isTestBaseClass]) return;
+
+ NSArray<FSTMutation *> *mutations = @[
+ FSTTestSetMutation(@"fob/bar",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"foo/bar",
+ @{ @"a" : @1 }),
+ FSTTestPatchMutation(@"foo/bar",
+ @{ @"b" : @1 }, nil),
+ FSTTestSetMutation(@"foo/bar/suffix/key",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"foo/baz",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"food/bar",
+ @{ @"a" : @1 })
+ ];
+
+ // Store all the mutations.
+ NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"];
+ for (FSTMutation *mutation in mutations) {
+ FSTMutationBatch *batch =
+ [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ mutation ]
+ group:group];
+ [batches addObject:batch];
+ }
+ [self.persistence commitGroup:group];
+
+ NSArray<FSTMutationBatch *> *expected = @[ batches[1], batches[2] ];
+ NSArray<FSTMutationBatch *> *matches =
+ [self.mutationQueue allMutationBatchesAffectingDocumentKey:FSTTestDocKey(@"foo/bar")];
+
+ XCTAssertEqualObjects(matches, expected);
+}
+
+- (void)testAllMutationBatchesAffectingQuery {
+ if ([self isTestBaseClass]) return;
+
+ NSArray<FSTMutation *> *mutations = @[
+ FSTTestSetMutation(@"fob/bar",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"foo/bar",
+ @{ @"a" : @1 }),
+ FSTTestPatchMutation(@"foo/bar",
+ @{ @"b" : @1 }, nil),
+ FSTTestSetMutation(@"foo/bar/suffix/key",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"foo/baz",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"food/bar",
+ @{ @"a" : @1 })
+ ];
+
+ // Store all the mutations.
+ NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"];
+ for (FSTMutation *mutation in mutations) {
+ FSTMutationBatch *batch =
+ [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ mutation ]
+ group:group];
+ [batches addObject:batch];
+ }
+ [self.persistence commitGroup:group];
+
+ NSArray<FSTMutationBatch *> *expected = @[ batches[1], batches[2], batches[4] ];
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo")];
+ NSArray<FSTMutationBatch *> *matches =
+ [self.mutationQueue allMutationBatchesAffectingQuery:query];
+
+ XCTAssertEqualObjects(matches, expected);
+}
+
+- (void)testRemoveMutationBatches {
+ if ([self isTestBaseClass]) return;
+
+ NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10];
+ FSTMutationBatch *last = batches[batches.count - 1];
+
+ [self removeMutationBatches:@[ batches[0] ]];
+ [batches removeObjectAtIndex:0];
+ XCTAssertEqual([self batchCount], 9);
+
+ NSArray<FSTMutationBatch *> *found;
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, batches);
+ XCTAssertEqual(found.count, 9);
+
+ [self removeMutationBatches:@[ batches[0], batches[1], batches[2] ]];
+ [batches removeObjectsInRange:NSMakeRange(0, 3)];
+ XCTAssertEqual([self batchCount], 6);
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, batches);
+ XCTAssertEqual(found.count, 6);
+
+ [self removeMutationBatches:@[ batches[batches.count - 1] ]];
+ [batches removeObjectAtIndex:batches.count - 1];
+ XCTAssertEqual([self batchCount], 5);
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, batches);
+ XCTAssertEqual(found.count, 5);
+
+ [self removeMutationBatches:@[ batches[3] ]];
+ [batches removeObjectAtIndex:3];
+ XCTAssertEqual([self batchCount], 4);
+
+ [self removeMutationBatches:@[ batches[1] ]];
+ [batches removeObjectAtIndex:1];
+ XCTAssertEqual([self batchCount], 3);
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, batches);
+ XCTAssertEqual(found.count, 3);
+ XCTAssertFalse([self.mutationQueue isEmpty]);
+
+ [self removeMutationBatches:batches];
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, @[]);
+ XCTAssertEqual(found.count, 0);
+ XCTAssertTrue([self.mutationQueue isEmpty]);
+}
+
+- (void)testRemoveMutationBatchesEmitsGarbageEvents {
+ if ([self isTestBaseClass]) return;
+
+ FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init];
+ [garbageCollector addGarbageSource:self.mutationQueue];
+
+ NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
+ [batches addObjectsFromArray:@[
+ [self addMutationBatchWithKey:@"foo/bar"],
+ [self addMutationBatchWithKey:@"foo/ba"],
+ [self addMutationBatchWithKey:@"foo/bar2"],
+ [self addMutationBatchWithKey:@"foo/bar"],
+ [self addMutationBatchWithKey:@"foo/bar/suffix/baz"],
+ [self addMutationBatchWithKey:@"bar/baz"],
+ ]];
+
+ [self removeMutationBatches:@[ batches[0] ]];
+ NSSet<FSTDocumentKey *> *garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[]);
+
+ [self removeMutationBatches:@[ batches[1] ]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/ba") ]);
+
+ [self removeMutationBatches:@[ batches[5] ]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"bar/baz") ]);
+
+ [self removeMutationBatches:@[ batches[2], batches[3] ]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/bar2") ]));
+
+ [batches addObject:[self addMutationBatchWithKey:@"foo/bar/suffix/baz"]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[]);
+
+ [self removeMutationBatches:@[ batches[4], batches[6] ]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/bar/suffix/baz") ]);
+}
+
+- (void)testStreamToken {
+ if ([self isTestBaseClass]) return;
+
+ NSData *streamToken1 = [@"token1" dataUsingEncoding:NSUTF8StringEncoding];
+ NSData *streamToken2 = [@"token2" dataUsingEncoding:NSUTF8StringEncoding];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"initial stream token"];
+ [self.mutationQueue setLastStreamToken:streamToken1 group:group];
+ [self.persistence commitGroup:group];
+
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+ [self addMutationBatch];
+
+ XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken1);
+
+ group = [self.persistence startGroupWithAction:@"acknowledgeBatchID"];
+ [self.mutationQueue acknowledgeBatch:batch1 streamToken:streamToken2 group:group];
+ [self.persistence commitGroup:group];
+
+ XCTAssertEqual(self.mutationQueue.highestAcknowledgedBatchID, batch1.batchID);
+ XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken2);
+}
+
+/** Creates a new FSTMutationBatch with the next batch ID and a set of dummy mutations. */
+- (FSTMutationBatch *)addMutationBatch {
+ return [self addMutationBatchWithKey:@"foo/bar"];
+}
+
+/**
+ * Creates a new FSTMutationBatch with the given key, the next batch ID and a set of dummy
+ * mutations.
+ */
+- (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key {
+ FSTSetMutation *mutation = FSTTestSetMutation(key, @{ @"a" : @1 });
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"];
+ FSTMutationBatch *batch =
+ [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ mutation ]
+ group:group];
+ [self.persistence commitGroup:group];
+ return batch;
+}
+
+/**
+ * Creates an array of batches containing @a number dummy FSTMutationBatches. Each has a different
+ * batchID.
+ */
+- (NSMutableArray<FSTMutationBatch *> *)createBatches:(int)number {
+ NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
+
+ for (int i = 0; i < number; i++) {
+ FSTMutationBatch *batch = [self addMutationBatch];
+ [batches addObject:batch];
+ }
+
+ return batches;
+}
+
+/**
+ * Calls -acknowledgeBatch:streamToken:group: on the mutation queue in a new group and commits the
+ * the group.
+ */
+- (void)acknowledgeBatch:(FSTMutationBatch *)batch {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Ack batchID"];
+ [self.mutationQueue acknowledgeBatch:batch streamToken:nil group:group];
+ [self.persistence commitGroup:group];
+}
+
+/**
+ * Calls -removeMutationBatches:group: on the mutation queue in a new group and commits the group.
+ */
+- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Remove mutation batch"];
+ [self.mutationQueue removeMutationBatches:batches group:group];
+ [self.persistence commitGroup:group];
+}
+
+/** Returns the number of mutation batches in the mutation queue. */
+- (NSUInteger)batchCount {
+ return [self.mutationQueue allMutationBatches].count;
+}
+
+/**
+ * Removes entries from from the given @a batches and returns them.
+ *
+ * @param holes An array of indexes in the batches array; in increasing order. Indexes are relative
+ * to the original state of the batches array, not any intermediate state that might occur.
+ * @param batches The array to mutate, removing entries from it.
+ * @return A new array containing all the entries that were removed from @a batches.
+ */
+- (NSArray<FSTMutationBatch *> *)makeHoles:(NSArray<NSNumber *> *)holes
+ inBatches:(NSMutableArray<FSTMutationBatch *> *)batches {
+ NSMutableArray<FSTMutationBatch *> *removed = [NSMutableArray array];
+ for (NSUInteger i = 0; i < holes.count; i++) {
+ NSUInteger index = holes[i].unsignedIntegerValue - i;
+ FSTMutationBatch *batch = batches[index];
+ [self removeMutationBatches:@[ batch ]];
+
+ [batches removeObjectAtIndex:index];
+ [removed addObject:batch];
+ }
+ return removed;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h
new file mode 100644
index 0000000..936bacf
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.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 FSTLevelDB;
+@class FSTMemoryPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTPersistenceTestHelpers : NSObject
+
+/**
+ * Creates and starts a new FSTLevelDB instance for testing, destroying any previous contents
+ * if they existed.
+ *
+ * Note that in order to avoid generating a bunch of garbage on the filesystem, the path of the
+ * database is reused. This prevents concurrent running of tests using this database. We may
+ * need to revisit this if we want to parallelize the tests.
+ */
++ (FSTLevelDB *)levelDBPersistence;
+
+/** Creates and starts a new FSTMemoryPersistence instance for testing. */
++ (FSTMemoryPersistence *)memoryPersistence;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m
new file mode 100644
index 0000000..f3d7914
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.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 "FSTPersistenceTestHelpers.h"
+
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTLocalSerializer.h"
+#import "Local/FSTMemoryPersistence.h"
+#import "Model/FSTDatabaseID.h"
+#import "Remote/FSTSerializerBeta.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTPersistenceTestHelpers
+
++ (FSTLevelDB *)levelDBPersistence {
+ NSError *error;
+ NSFileManager *files = [NSFileManager defaultManager];
+
+ NSString *dir =
+ [NSTemporaryDirectory() stringByAppendingPathComponent:@"FSTPersistenceTestHelpers"];
+ if ([files fileExistsAtPath:dir]) {
+ // Delete the directory first to ensure isolation between runs.
+ BOOL success = [files removeItemAtPath:dir error:&error];
+ if (!success) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"Failed to clean up leveldb path %@: %@", dir, error];
+ }
+ }
+
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ FSTSerializerBeta *remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID];
+ FSTLocalSerializer *serializer =
+ [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer];
+ FSTLevelDB *db = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer];
+ BOOL success = [db start:&error];
+ if (!success) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"Failed to create leveldb path %@: %@", dir, error];
+ }
+
+ return db;
+}
+
++ (FSTMemoryPersistence *)memoryPersistence {
+ NSError *error;
+ FSTMemoryPersistence *persistence = [FSTMemoryPersistence persistence];
+ BOOL success = [persistence start:&error];
+ if (!success) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"Failed to start memory persistence: %@", error];
+ }
+
+ return persistence;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.h b/Firestore/Example/Tests/Local/FSTQueryCacheTests.h
new file mode 100644
index 0000000..a615372
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.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 "Local/FSTQueryCache.h"
+
+#import <XCTest/XCTest.h>
+
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * These are tests for any implementation of the FSTQueryCache protocol.
+ *
+ * To test a specific implementation of FSTQueryCache:
+ *
+ * + Subclass FSTQueryCacheTests
+ * + override -setUp, assigning to queryCache and persistence
+ * + override -tearDown, cleaning up queryCache and persistence
+ */
+@interface FSTQueryCacheTests : XCTestCase
+
+/** The implementation of the query cache to test. */
+@property(nonatomic, strong, nullable) id<FSTQueryCache> queryCache;
+
+/**
+ * The persistence implementation to use while testing the queryCache (e.g. for committing write
+ * groups).
+ */
+@property(nonatomic, strong, nullable) id<FSTPersistence> persistence;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m
new file mode 100644
index 0000000..ed409b4
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTQueryCacheTests.h"
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Local/FSTEagerGarbageCollector.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTQueryData.h"
+#import "Local/FSTWriteGroup.h"
+#import "Model/FSTDocumentKey.h"
+
+#import "FSTHelpers.h"
+#import "FSTImmutableSortedSet+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTQueryCacheTests {
+ FSTQuery *_queryRooms;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ _queryRooms = FSTTestQuery(@"rooms");
+}
+
+/**
+ * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for
+ * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses.
+ */
+- (BOOL)isTestBaseClass {
+ return [self class] == [FSTQueryCacheTests class];
+}
+
+- (void)testReadQueryNotInCache {
+ if ([self isTestBaseClass]) return;
+
+ XCTAssertNil([self.queryCache queryDataForQuery:_queryRooms]);
+}
+
+- (void)testSetAndReadAQuery {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+ [self addQueryData:queryData];
+
+ FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms];
+ XCTAssertEqualObjects(result.query, queryData.query);
+ XCTAssertEqual(result.targetID, queryData.targetID);
+ XCTAssertEqualObjects(result.resumeToken, queryData.resumeToken);
+}
+
+- (void)testCanonicalIDCollision {
+ if ([self isTestBaseClass]) return;
+
+ // Type information is currently lost in our canonicalID implementations so this currently an
+ // easy way to force colliding canonicalIDs
+ FSTQuery *q1 = [[FSTQuery queryWithPath:FSTTestPath(@"a")]
+ queryByAddingFilter:FSTTestFilter(@"foo", @"==", @(1))];
+ FSTQuery *q2 = [[FSTQuery queryWithPath:FSTTestPath(@"a")]
+ queryByAddingFilter:FSTTestFilter(@"foo", @"==", @"1")];
+ XCTAssertEqualObjects(q1.canonicalID, q2.canonicalID);
+
+ FSTQueryData *data1 = [self queryDataWithQuery:q1 targetID:1 version:1];
+ [self addQueryData:data1];
+
+ // Using the other query should not return the query cache entry despite equal canonicalIDs.
+ XCTAssertNil([self.queryCache queryDataForQuery:q2]);
+ XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1);
+
+ FSTQueryData *data2 = [self queryDataWithQuery:q2 targetID:2 version:1];
+ [self addQueryData:data2];
+
+ XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1);
+ XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2);
+
+ [self removeQueryData:data1];
+ XCTAssertNil([self.queryCache queryDataForQuery:q1]);
+ XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2);
+
+ [self removeQueryData:data2];
+ XCTAssertNil([self.queryCache queryDataForQuery:q1]);
+ XCTAssertNil([self.queryCache queryDataForQuery:q2]);
+}
+
+- (void)testSetQueryToNewValue {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+ [self addQueryData:queryData1];
+
+ FSTQueryData *queryData2 = [self queryDataWithQuery:_queryRooms targetID:1 version:2];
+ [self addQueryData:queryData2];
+
+ FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms];
+ XCTAssertNotEqualObjects(queryData2.resumeToken, queryData1.resumeToken);
+ XCTAssertNotEqualObjects(queryData2.snapshotVersion, queryData1.snapshotVersion);
+ XCTAssertEqualObjects(result.resumeToken, queryData2.resumeToken);
+ XCTAssertEqualObjects(result.snapshotVersion, queryData2.snapshotVersion);
+}
+
+- (void)testRemoveQuery {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+ [self addQueryData:queryData1];
+
+ [self removeQueryData:queryData1];
+
+ FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms];
+ XCTAssertNil(result);
+}
+
+- (void)testRemoveNonExistentQuery {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+
+ // no-op, but make sure it doesn't throw.
+ XCTAssertNoThrow([self removeQueryData:queryData]);
+}
+
+- (void)testRemoveQueryRemovesMatchingKeysToo {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *rooms = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+ [self addQueryData:rooms];
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/foo"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/bar"];
+ [self addMatchingKey:key1 forTargetID:rooms.targetID];
+ [self addMatchingKey:key2 forTargetID:rooms.targetID];
+
+ XCTAssertTrue([self.queryCache containsKey:key1]);
+ XCTAssertTrue([self.queryCache containsKey:key2]);
+
+ [self removeQueryData:rooms];
+ XCTAssertFalse([self.queryCache containsKey:key1]);
+ XCTAssertFalse([self.queryCache containsKey:key2]);
+}
+
+- (void)testAddOrRemoveMatchingKeys {
+ if ([self isTestBaseClass]) return;
+
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+
+ XCTAssertFalse([self.queryCache containsKey:key]);
+
+ [self addMatchingKey:key forTargetID:1];
+ XCTAssertTrue([self.queryCache containsKey:key]);
+
+ [self addMatchingKey:key forTargetID:2];
+ XCTAssertTrue([self.queryCache containsKey:key]);
+
+ [self removeMatchingKey:key forTargetID:1];
+ XCTAssertTrue([self.queryCache containsKey:key]);
+
+ [self removeMatchingKey:key forTargetID:2];
+ XCTAssertFalse([self.queryCache containsKey:key]);
+}
+
+- (void)testRemoveMatchingKeysForTargetID {
+ if ([self isTestBaseClass]) return;
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"];
+
+ [self addMatchingKey:key1 forTargetID:1];
+ [self addMatchingKey:key2 forTargetID:1];
+ [self addMatchingKey:key3 forTargetID:2];
+ XCTAssertTrue([self.queryCache containsKey:key1]);
+ XCTAssertTrue([self.queryCache containsKey:key2]);
+ XCTAssertTrue([self.queryCache containsKey:key3]);
+
+ [self removeMatchingKeysForTargetID:1];
+ XCTAssertFalse([self.queryCache containsKey:key1]);
+ XCTAssertFalse([self.queryCache containsKey:key2]);
+ XCTAssertTrue([self.queryCache containsKey:key3]);
+
+ [self removeMatchingKeysForTargetID:2];
+ XCTAssertFalse([self.queryCache containsKey:key1]);
+ XCTAssertFalse([self.queryCache containsKey:key2]);
+ XCTAssertFalse([self.queryCache containsKey:key3]);
+}
+
+- (void)testRemoveEmitsGarbageEvents {
+ if ([self isTestBaseClass]) return;
+
+ FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init];
+ [garbageCollector addGarbageSource:self.queryCache];
+ FSTAssertEqualSets([garbageCollector collectGarbage], @[]);
+
+ FSTQueryData *rooms = [self queryDataWithQuery:FSTTestQuery(@"rooms") targetID:1 version:1];
+ FSTDocumentKey *room1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"];
+ FSTDocumentKey *room2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"];
+ [self addQueryData:rooms];
+ [self addMatchingKey:room1 forTargetID:rooms.targetID];
+ [self addMatchingKey:room2 forTargetID:rooms.targetID];
+
+ FSTQueryData *halls = [self queryDataWithQuery:FSTTestQuery(@"halls") targetID:2 version:1];
+ FSTDocumentKey *hall1 = [FSTDocumentKey keyWithPathString:@"halls/bar"];
+ FSTDocumentKey *hall2 = [FSTDocumentKey keyWithPathString:@"halls/foo"];
+ [self addQueryData:halls];
+ [self addMatchingKey:hall1 forTargetID:halls.targetID];
+ [self addMatchingKey:hall2 forTargetID:halls.targetID];
+
+ FSTAssertEqualSets([garbageCollector collectGarbage], @[]);
+
+ [self removeMatchingKey:room1 forTargetID:rooms.targetID];
+ FSTAssertEqualSets([garbageCollector collectGarbage], @[ room1 ]);
+
+ [self removeQueryData:rooms];
+ FSTAssertEqualSets([garbageCollector collectGarbage], @[ room2 ]);
+
+ [self removeMatchingKeysForTargetID:halls.targetID];
+ FSTAssertEqualSets([garbageCollector collectGarbage], (@[ hall1, hall2 ]));
+}
+
+- (void)testMatchingKeysForTargetID {
+ if ([self isTestBaseClass]) return;
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"];
+
+ [self addMatchingKey:key1 forTargetID:1];
+ [self addMatchingKey:key2 forTargetID:1];
+ [self addMatchingKey:key3 forTargetID:2];
+
+ FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ]));
+ FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], @[ key3 ]);
+
+ [self addMatchingKey:key1 forTargetID:2];
+ FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ]));
+ FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], (@[ key1, key3 ]));
+}
+
+- (void)testHighestTargetID {
+ if ([self isTestBaseClass]) return;
+
+ XCTAssertEqual([self.queryCache highestTargetID], 0);
+
+ FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"rooms")
+ targetID:1
+ purpose:FSTQueryPurposeListen];
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"];
+ [self addQueryData:query1];
+ [self addMatchingKey:key1 forTargetID:1];
+ [self addMatchingKey:key2 forTargetID:1];
+
+ FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"halls")
+ targetID:2
+ purpose:FSTQueryPurposeListen];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"halls/foo"];
+ [self addQueryData:query2];
+ [self addMatchingKey:key3 forTargetID:2];
+ XCTAssertEqual([self.queryCache highestTargetID], 2);
+
+ // TargetIDs never come down.
+ [self removeQueryData:query2];
+ XCTAssertEqual([self.queryCache highestTargetID], 2);
+
+ // A query with an empty result set still counts.
+ FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"garages")
+ targetID:42
+ purpose:FSTQueryPurposeListen];
+ [self addQueryData:query3];
+ XCTAssertEqual([self.queryCache highestTargetID], 42);
+
+ [self removeQueryData:query1];
+ XCTAssertEqual([self.queryCache highestTargetID], 42);
+
+ [self removeQueryData:query3];
+ XCTAssertEqual([self.queryCache highestTargetID], 42);
+
+ // Verify that the highestTargetID even survives restarts.
+ [self.queryCache shutdown];
+ self.queryCache = [self.persistence queryCache];
+ [self.queryCache start];
+ XCTAssertEqual([self.queryCache highestTargetID], 42);
+}
+
+- (void)testLastRemoteSnapshotVersion {
+ if ([self isTestBaseClass]) return;
+
+ XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion],
+ [FSTSnapshotVersion noVersion]);
+
+ // Can set the snapshot version.
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"setLastRemoteSnapshotVersion"];
+ [self.queryCache setLastRemoteSnapshotVersion:FSTTestVersion(42) group:group];
+ [self.persistence commitGroup:group];
+ XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42));
+
+ // Snapshot version persists restarts.
+ self.queryCache = [self.persistence queryCache];
+ [self.queryCache start];
+ XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42));
+}
+
+#pragma mark - Helpers
+
+/**
+ * Creates a new FSTQueryData object from the given parameters, synthesizing a resume token from
+ * the snapshot version.
+ */
+- (FSTQueryData *)queryDataWithQuery:(FSTQuery *)query
+ targetID:(FSTTargetID)targetID
+ version:(FSTTestSnapshotVersion)version {
+ NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(version);
+ return [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:FSTTestVersion(version)
+ resumeToken:resumeToken];
+}
+
+/** Adds the given query data to the queryCache under test, committing immediately. */
+- (void)addQueryData:(FSTQueryData *)queryData {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addQueryData"];
+ [self.queryCache addQueryData:queryData group:group];
+ [self.persistence commitGroup:group];
+}
+
+/** Removes the given query data from the queryCache under test, committing immediately. */
+- (void)removeQueryData:(FSTQueryData *)queryData {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeQueryData"];
+ [self.queryCache removeQueryData:queryData group:group];
+ [self.persistence commitGroup:group];
+}
+
+- (void)addMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID {
+ FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet];
+ keys = [keys setByAddingObject:key];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addMatchingKeys"];
+ [self.queryCache addMatchingKeys:keys forTargetID:targetID group:group];
+ [self.persistence commitGroup:group];
+}
+
+- (void)removeMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID {
+ FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet];
+ keys = [keys setByAddingObject:key];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeys"];
+ [self.queryCache removeMatchingKeys:keys forTargetID:targetID group:group];
+ [self.persistence commitGroup:group];
+}
+
+- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeysForTargetID"];
+ [self.queryCache removeMatchingKeysForTargetID:targetID group:group];
+ [self.persistence commitGroup:group];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTReferenceSetTests.m b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m
new file mode 100644
index 0000000..a8c783a
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTReferenceSet.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTDocumentKey.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTReferenceSetTests : XCTestCase
+@end
+
+@implementation FSTReferenceSetTests
+
+- (void)testAddOrRemoveReferences {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+
+ FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init];
+ XCTAssertTrue([referenceSet isEmpty]);
+ XCTAssertFalse([referenceSet containsKey:key]);
+
+ [referenceSet addReferenceToKey:key forID:1];
+ XCTAssertTrue([referenceSet containsKey:key]);
+ XCTAssertFalse([referenceSet isEmpty]);
+
+ [referenceSet addReferenceToKey:key forID:2];
+ XCTAssertTrue([referenceSet containsKey:key]);
+
+ [referenceSet removeReferenceToKey:key forID:1];
+ XCTAssertTrue([referenceSet containsKey:key]);
+
+ [referenceSet removeReferenceToKey:key forID:3];
+ XCTAssertTrue([referenceSet containsKey:key]);
+
+ [referenceSet removeReferenceToKey:key forID:2];
+ XCTAssertFalse([referenceSet containsKey:key]);
+ XCTAssertTrue([referenceSet isEmpty]);
+}
+
+- (void)testRemoveAllReferencesForTargetID {
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"];
+ FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init];
+
+ [referenceSet addReferenceToKey:key1 forID:1];
+ [referenceSet addReferenceToKey:key2 forID:1];
+ [referenceSet addReferenceToKey:key3 forID:2];
+ XCTAssertFalse([referenceSet isEmpty]);
+ XCTAssertTrue([referenceSet containsKey:key1]);
+ XCTAssertTrue([referenceSet containsKey:key2]);
+ XCTAssertTrue([referenceSet containsKey:key3]);
+
+ [referenceSet removeReferencesForID:1];
+ XCTAssertFalse([referenceSet isEmpty]);
+ XCTAssertFalse([referenceSet containsKey:key1]);
+ XCTAssertFalse([referenceSet containsKey:key2]);
+ XCTAssertTrue([referenceSet containsKey:key3]);
+
+ [referenceSet removeReferencesForID:2];
+ XCTAssertTrue([referenceSet isEmpty]);
+ XCTAssertFalse([referenceSet containsKey:key1]);
+ XCTAssertFalse([referenceSet containsKey:key2]);
+ XCTAssertFalse([referenceSet containsKey:key3]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h
new file mode 100644
index 0000000..fa2a857
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.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 "Local/FSTRemoteDocumentCache.h"
+
+#import <XCTest/XCTest.h>
+
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * These are tests for any implementation of the FSTRemoteDocumentCache protocol.
+ *
+ * To test a specific implementation of FSTRemoteDocumentCache:
+ *
+ * + Subclass FSTRemoteDocumentCacheTests
+ * + override -setUp, assigning to remoteDocumentCache and persistence
+ * + override -tearDown, cleaning up remoteDocumentCache and persistence
+ */
+@interface FSTRemoteDocumentCacheTests : XCTestCase
+@property(nonatomic, strong, nullable) id<FSTRemoteDocumentCache> remoteDocumentCache;
+@property(nonatomic, strong, nullable) id<FSTPersistence> persistence;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m
new file mode 100644
index 0000000..a875934
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTRemoteDocumentCacheTests.h"
+
+#import "Core/FSTQuery.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTWriteGroup.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTDocumentSet.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const kDocPath = @"a/b";
+static NSString *const kLongDocPath = @"a/b/c/d/e/f";
+static const int kVersion = 42;
+
+@implementation FSTRemoteDocumentCacheTests {
+ NSDictionary<NSString *, id> *_kDocData;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ // essentially a constant, but can't be a compile-time one.
+ _kDocData = @{ @"a" : @1, @"b" : @2 };
+}
+
+- (void)testReadDocumentNotInCache {
+ if (!self.remoteDocumentCache) return;
+
+ XCTAssertNil([self readEntryAtPath:kDocPath]);
+}
+
+// Helper for next two tests.
+- (void)setAndReadADocumentAtPath:(NSString *)path {
+ FSTDocument *written = [self setTestDocumentAtPath:path];
+ FSTMaybeDocument *read = [self readEntryAtPath:path];
+ XCTAssertEqualObjects(read, written);
+}
+
+- (void)testSetAndReadADocument {
+ if (!self.remoteDocumentCache) return;
+
+ [self setAndReadADocumentAtPath:kDocPath];
+}
+
+- (void)testSetAndReadADocumentAtDeepPath {
+ if (!self.remoteDocumentCache) return;
+
+ [self setAndReadADocumentAtPath:kLongDocPath];
+}
+
+- (void)testSetAndReadDeletedDocument {
+ if (!self.remoteDocumentCache) return;
+
+ FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(kDocPath, kVersion);
+ [self addEntry:deletedDoc];
+
+ XCTAssertEqualObjects([self readEntryAtPath:kDocPath], deletedDoc);
+}
+
+- (void)testSetDocumentToNewValue {
+ if (!self.remoteDocumentCache) return;
+
+ [self setTestDocumentAtPath:kDocPath];
+ FSTDocument *newDoc = FSTTestDoc(kDocPath, kVersion, @{ @"data" : @2 }, NO);
+ [self addEntry:newDoc];
+ XCTAssertEqualObjects([self readEntryAtPath:kDocPath], newDoc);
+}
+
+- (void)testRemoveDocument {
+ if (!self.remoteDocumentCache) return;
+
+ [self setTestDocumentAtPath:kDocPath];
+ [self removeEntryAtPath:kDocPath];
+
+ XCTAssertNil([self readEntryAtPath:kDocPath]);
+}
+
+- (void)testRemoveNonExistentDocument {
+ if (!self.remoteDocumentCache) return;
+
+ // no-op, but make sure it doesn't throw.
+ XCTAssertNoThrow([self removeEntryAtPath:kDocPath]);
+}
+
+// TODO(mikelehen): Write more elaborate tests once we have more elaborate implementations.
+- (void)testDocumentsMatchingQuery {
+ if (!self.remoteDocumentCache) return;
+
+ [self setTestDocumentAtPath:@"a/1"];
+ [self setTestDocumentAtPath:@"b/1"];
+ [self setTestDocumentAtPath:@"b/2"];
+ [self setTestDocumentAtPath:@"c/1"];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"b")];
+ FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query];
+ NSArray *expected =
+ @[ FSTTestDoc(@"b/1", kVersion, _kDocData, NO), FSTTestDoc(@"b/2", kVersion, _kDocData, NO) ];
+ for (FSTDocument *doc in expected) {
+ XCTAssertEqualObjects([results objectForKey:doc.key], doc);
+ }
+
+ // TODO(mikelehen): Perhaps guard against extra documents in the result set once our
+ // implementations are smarter.
+}
+
+#pragma mark - Helpers
+
+- (FSTDocument *)setTestDocumentAtPath:(NSString *)path {
+ FSTDocument *doc = FSTTestDoc(path, kVersion, _kDocData, NO);
+ [self addEntry:doc];
+ return doc;
+}
+
+- (void)addEntry:(FSTMaybeDocument *)maybeDoc {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addEntry"];
+ [self.remoteDocumentCache addEntry:maybeDoc group:group];
+ [self.persistence commitGroup:group];
+}
+
+- (FSTMaybeDocument *_Nullable)readEntryAtPath:(NSString *)path {
+ return [self.remoteDocumentCache entryForKey:FSTTestDocKey(path)];
+}
+
+- (void)removeEntryAtPath:(NSString *)path {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeEntryAtPath"];
+ [self.remoteDocumentCache removeEntryForKey:FSTTestDocKey(path) group:group];
+ [self.persistence commitGroup:group];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m
new file mode 100644
index 0000000..ebf7713
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTRemoteDocumentChangeBuffer.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTRemoteDocumentCache.h"
+#import "Model/FSTDocument.h"
+
+#import "FSTHelpers.h"
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTRemoteDocumentChangeBufferTests : XCTestCase
+@end
+
+@implementation FSTRemoteDocumentChangeBufferTests {
+ FSTLevelDB *_db;
+ id<FSTRemoteDocumentCache> _remoteDocumentCache;
+ FSTRemoteDocumentChangeBuffer *_remoteDocumentBuffer;
+
+ FSTMaybeDocument *_kInitialADoc;
+ FSTMaybeDocument *_kInitialBDoc;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ _db = [FSTPersistenceTestHelpers levelDBPersistence];
+ _remoteDocumentCache = [_db remoteDocumentCache];
+
+ // Add a couple initial items to the cache.
+ FSTWriteGroup *group = [_db startGroupWithAction:@"Add initial docs."];
+ _kInitialADoc = FSTTestDoc(@"coll/a", 42, @{@"test" : @"data"}, NO);
+ [_remoteDocumentCache addEntry:_kInitialADoc group:group];
+
+ _kInitialBDoc =
+ [FSTDeletedDocument documentWithKey:FSTTestDocKey(@"coll/b") version:FSTTestVersion(314)];
+ [_remoteDocumentCache addEntry:_kInitialBDoc group:group];
+ [_db commitGroup:group];
+
+ _remoteDocumentBuffer =
+ [FSTRemoteDocumentChangeBuffer changeBufferWithCache:_remoteDocumentCache];
+}
+
+- (void)tearDown {
+ _remoteDocumentBuffer = nil;
+ _remoteDocumentCache = nil;
+ _db = nil;
+
+ [super tearDown];
+}
+
+- (void)testReadUnchangedEntry {
+ XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")],
+ _kInitialADoc);
+}
+
+- (void)testAddEntryAndReadItBack {
+ FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO);
+ [_remoteDocumentBuffer addEntry:newADoc];
+ XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc);
+
+ // B should still be unchanged.
+ XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/b")],
+ _kInitialBDoc);
+}
+
+- (void)testApplyChanges {
+ FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO);
+ [_remoteDocumentBuffer addEntry:newADoc];
+ XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc);
+
+ // Reading directly against the cache should still yield the old result.
+ XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], _kInitialADoc);
+
+ FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"];
+ [_remoteDocumentBuffer applyToWriteGroup:group];
+ [_db commitGroup:group];
+
+ // Reading against the cache should now yield the new result.
+ XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], newADoc);
+}
+
+- (void)testMethodsThrowAfterApply {
+ FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"];
+ [_remoteDocumentBuffer applyToWriteGroup:group];
+ [_db commitGroup:group];
+
+ XCTAssertThrows([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")]);
+ XCTAssertThrows([_remoteDocumentBuffer addEntry:_kInitialADoc]);
+ XCTAssertThrows([_remoteDocumentBuffer applyToWriteGroup:group]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm b/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm
new file mode 100644
index 0000000..1cd2feb
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTWriteGroup.h"
+
+#import <XCTest/XCTest.h>
+#include <leveldb/db.h>
+
+#import "Protos/objc/firestore/local/Mutation.pbobjc.h"
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTLevelDBKey.h"
+
+#import "FSTPersistenceTestHelpers.h"
+
+using leveldb::ReadOptions;
+using leveldb::Status;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTWriteGroupTests : XCTestCase
+@end
+
+@implementation FSTWriteGroupTests {
+ FSTLevelDB *_db;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ _db = [FSTPersistenceTestHelpers levelDBPersistence];
+}
+
+- (void)tearDown {
+ _db = nil;
+
+ [super tearDown];
+}
+
+- (void)testCommit {
+ std::string key = [FSTLevelDBMutationKey keyWithUserID:"user1" batchID:42];
+ FSTPBWriteBatch *message = [FSTPBWriteBatch message];
+ message.batchId = 42;
+
+ // This is a test that shows that committing an empty group does not fail. There are no side
+ // effects to verify though.
+ FSTWriteGroup *group = [_db startGroupWithAction:@"Empty commit"];
+ XCTAssertNoThrow([_db commitGroup:group]);
+
+ group = [_db startGroupWithAction:@"Put"];
+ [group setMessage:message forKey:key];
+
+ std::string value;
+ Status status = _db.ptr->Get(ReadOptions(), key, &value);
+ XCTAssertTrue(status.IsNotFound());
+
+ [_db commitGroup:group];
+ status = _db.ptr->Get(ReadOptions(), key, &value);
+ XCTAssertTrue(status.ok());
+
+ group = [_db startGroupWithAction:@"Delete"];
+ [group removeMessageForKey:key];
+ status = _db.ptr->Get(ReadOptions(), key, &value);
+ XCTAssertTrue(status.ok());
+
+ [_db commitGroup:group];
+ status = _db.ptr->Get(ReadOptions(), key, &value);
+ XCTAssertTrue(status.IsNotFound());
+}
+
+- (void)testDescription {
+ std::string key = [FSTLevelDBMutationKey keyWithUserID:"user1" batchID:42];
+ FSTPBWriteBatch *message = [FSTPBWriteBatch message];
+ message.batchId = 42;
+
+ FSTWriteGroup *group = [FSTWriteGroup groupWithAction:@"Action"];
+ XCTAssertEqualObjects([group description], @"<FSTWriteGroup for Action: 0 changes (0 bytes):>");
+
+ [group setMessage:message forKey:key];
+ XCTAssertEqualObjects([group description],
+ @"<FSTWriteGroup for Action: 1 changes (2 bytes):\n"
+ " - Put [mutation: userID=user1 batchID=42] (2 bytes)>");
+
+ [group removeMessageForKey:key];
+ XCTAssertEqualObjects([group description],
+ @"<FSTWriteGroup for Action: 2 changes (2 bytes):\n"
+ " - Put [mutation: userID=user1 batchID=42] (2 bytes)\n"
+ " - Delete [mutation: userID=user1 batchID=42]>");
+}
+
+- (void)testCommittingWrongGroupThrows {
+ // If you don't create the group through persistence, it should throw.
+ FSTWriteGroup *group = [FSTWriteGroup groupWithAction:@"group"];
+ XCTAssertThrows([_db commitGroup:group]);
+}
+
+- (void)testCommittingTwiceThrows {
+ FSTWriteGroup *group = [_db startGroupWithAction:@"group"];
+ [_db commitGroup:group];
+ XCTAssertThrows([_db commitGroup:group]);
+}
+
+- (void)testNestingGroupsThrows {
+ [_db startGroupWithAction:@"group1"];
+ XCTAssertThrows([_db startGroupWithAction:@"group2"]);
+}
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m
new file mode 100644
index 0000000..9e7299f
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m
@@ -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 "Model/FSTDatabaseID.h"
+
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDatabaseIDTests : XCTestCase
+@end
+
+@implementation FSTDatabaseIDTests
+
+- (void)testConstructor {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ XCTAssertEqualObjects(databaseID.projectID, @"p");
+ XCTAssertEqualObjects(databaseID.databaseID, @"d");
+ XCTAssertFalse([databaseID isDefaultDatabase]);
+}
+
+- (void)testDefaultDatabase {
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID];
+ XCTAssertEqualObjects(databaseID.projectID, @"p");
+ XCTAssertEqualObjects(databaseID.databaseID, @"(default)");
+ XCTAssertTrue([databaseID isDefaultDatabase]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m
new file mode 100644
index 0000000..ba68009
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.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 "Model/FSTDocumentKey.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTPath.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDocumentKeyTests : XCTestCase
+@end
+
+@implementation FSTDocumentKeyTests
+
+- (void)testConstructor {
+ FSTResourcePath *path =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"firestore", @"messages", @"1" ]];
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:path];
+ XCTAssertEqual(path, key.path);
+}
+
+- (void)testComparison {
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithSegments:@[ @"x", @"y", @"z", @"w" ]];
+ XCTAssertTrue([key1 isEqualToKey:key2]);
+ XCTAssertFalse([key1 isEqualToKey:key3]);
+
+ FSTDocumentKey *empty = [FSTDocumentKey keyWithSegments:@[]];
+ FSTDocumentKey *a = [FSTDocumentKey keyWithSegments:@[ @"a", @"a" ]];
+ FSTDocumentKey *b = [FSTDocumentKey keyWithSegments:@[ @"b", @"b" ]];
+ FSTDocumentKey *ab = [FSTDocumentKey keyWithSegments:@[ @"a", @"a", @"b", @"b" ]];
+
+ XCTAssertEqual(NSOrderedAscending, [empty compare:a]);
+ XCTAssertEqual(NSOrderedAscending, [a compare:b]);
+ XCTAssertEqual(NSOrderedAscending, [a compare:ab]);
+
+ XCTAssertEqual(NSOrderedDescending, [a compare:empty]);
+ XCTAssertEqual(NSOrderedDescending, [b compare:a]);
+ XCTAssertEqual(NSOrderedDescending, [ab compare:a]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTDocumentSetTests.m b/Firestore/Example/Tests/Model/FSTDocumentSetTests.m
new file mode 100644
index 0000000..e473088
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTDocumentSetTests.m
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Model/FSTDocumentSet.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTDocument.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDocumentSetTests : XCTestCase
+@end
+
+@implementation FSTDocumentSetTests {
+ NSComparator _comp;
+ FSTDocument *_doc1;
+ FSTDocument *_doc2;
+ FSTDocument *_doc3;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ _comp = FSTTestDocComparator(@"sort");
+ _doc1 = FSTTestDoc(@"docs/1", 0, @{ @"sort" : @2 }, NO);
+ _doc2 = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @3 }, NO);
+ _doc3 = FSTTestDoc(@"docs/3", 0, @{ @"sort" : @1 }, NO);
+}
+
+- (void)testCount {
+ XCTAssertEqual([FSTTestDocSet(_comp, @[]) count], 0);
+ XCTAssertEqual([FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]) count], 3);
+}
+
+- (void)testHasKey {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]);
+
+ XCTAssertTrue([set containsKey:_doc1.key]);
+ XCTAssertTrue([set containsKey:_doc2.key]);
+ XCTAssertFalse([set containsKey:_doc3.key]);
+}
+
+- (void)testDocumentForKey {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]);
+
+ XCTAssertEqualObjects([set documentForKey:_doc1.key], _doc1);
+ XCTAssertEqualObjects([set documentForKey:_doc2.key], _doc2);
+ XCTAssertNil([set documentForKey:_doc3.key]);
+}
+
+- (void)testFirstAndLastDocument {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[]);
+ XCTAssertNil([set firstDocument]);
+ XCTAssertNil([set lastDocument]);
+
+ set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+ XCTAssertEqualObjects([set firstDocument], _doc3);
+ XCTAssertEqualObjects([set lastDocument], _doc2);
+}
+
+- (void)testKeepsDocumentsInTheRightOrder {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+ XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ]));
+}
+
+- (void)testPredecessorDocumentForKey {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+
+ XCTAssertNil([set predecessorDocumentForKey:_doc3.key]);
+ XCTAssertEqualObjects([set predecessorDocumentForKey:_doc1.key], _doc3);
+ XCTAssertEqualObjects([set predecessorDocumentForKey:_doc2.key], _doc1);
+}
+
+- (void)testDeletes {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+
+ FSTDocumentSet *setWithoutDoc1 = [set documentSetByRemovingKey:_doc1.key];
+ XCTAssertEqualObjects([[setWithoutDoc1 documentEnumerator] allObjects], (@[ _doc3, _doc2 ]));
+ XCTAssertEqual([setWithoutDoc1 count], 2);
+
+ // Original remains unchanged
+ XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ]));
+
+ FSTDocumentSet *setWithoutDoc3 = [setWithoutDoc1 documentSetByRemovingKey:_doc3.key];
+ XCTAssertEqualObjects([[setWithoutDoc3 documentEnumerator] allObjects], (@[ _doc2 ]));
+ XCTAssertEqual([setWithoutDoc3 count], 1);
+}
+
+- (void)testUpdates {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+
+ FSTDocument *doc2Prime = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @9 }, NO);
+
+ set = [set documentSetByAddingDocument:doc2Prime];
+ XCTAssertEqual([set count], 3);
+ XCTAssertEqualObjects([set documentForKey:doc2Prime.key], doc2Prime);
+ XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, doc2Prime ]));
+}
+
+- (void)testAddsDocsWithEqualComparisonValues {
+ FSTDocument *doc4 = FSTTestDoc(@"docs/4", 0, @{ @"sort" : @2 }, NO);
+
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, doc4 ]);
+ XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc1, doc4 ]));
+}
+
+- (void)testIsEqual {
+ FSTDocumentSet *set1 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]);
+ FSTDocumentSet *set2 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]);
+ XCTAssertEqualObjects(set1, set1);
+ XCTAssertEqualObjects(set1, set2);
+ XCTAssertNotEqualObjects(set1, nil);
+
+ FSTDocumentSet *sortedSet1 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+ FSTDocumentSet *sortedSet2 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+ XCTAssertEqualObjects(sortedSet1, sortedSet1);
+ XCTAssertEqualObjects(sortedSet1, sortedSet2);
+ XCTAssertNotEqualObjects(sortedSet1, nil);
+
+ FSTDocumentSet *shortSet = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2 ]);
+ XCTAssertNotEqualObjects(set1, shortSet);
+ XCTAssertNotEqualObjects(set1, sortedSet1);
+}
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTDocumentTests.m b/Firestore/Example/Tests/Model/FSTDocumentTests.m
new file mode 100644
index 0000000..9ceb0cd
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTDocumentTests.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 "Model/FSTDocument.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTSnapshotVersion.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDocumentTests : XCTestCase
+@end
+
+@implementation FSTDocumentTests
+
+- (void)testConstructor {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"messages/first"];
+ FSTSnapshotVersion *version = FSTTestVersion(1);
+ FSTObjectValue *data = FSTTestObjectValue(@{ @"a" : @1 });
+ FSTDocument *doc =
+ [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO];
+
+ XCTAssertEqualObjects(doc.key, [FSTDocumentKey keyWithPathString:@"messages/first"]);
+ XCTAssertEqualObjects(doc.version, version);
+ XCTAssertEqualObjects(doc.data, data);
+ XCTAssertEqual(doc.hasLocalMutations, NO);
+}
+
+- (void)testExtractsFields {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"rooms/eros"];
+ FSTSnapshotVersion *version = FSTTestVersion(1);
+ FSTObjectValue *data = FSTTestObjectValue(@{
+ @"desc" : @"Discuss all the project related stuff",
+ @"owner" : @{@"name" : @"Jonny", @"title" : @"scallywag"}
+ });
+ FSTDocument *doc =
+ [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO];
+
+ XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"desc")],
+ [FSTStringValue stringValue:@"Discuss all the project related stuff"]);
+ XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"owner.title")],
+ [FSTStringValue stringValue:@"scallywag"]);
+}
+
+- (void)testIsEqual {
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"messages/first"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"messages/second"];
+ FSTObjectValue *data1 = FSTTestObjectValue(@{ @"a" : @1 });
+ FSTObjectValue *data2 = FSTTestObjectValue(@{ @"b" : @1 });
+ FSTSnapshotVersion *version1 = FSTTestVersion(1);
+
+ FSTDocument *doc1 =
+ [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO];
+ FSTDocument *doc2 =
+ [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO];
+
+ XCTAssertEqualObjects(doc1, doc2);
+ XCTAssertEqualObjects(
+ doc1, [FSTDocument documentWithData:FSTTestObjectValue(
+ @{ @"a" : @1 })
+ key:[FSTDocumentKey keyWithPathString:@"messages/first"]
+ version:version1
+ hasLocalMutations:NO]);
+
+ FSTSnapshotVersion *version2 = FSTTestVersion(2);
+ XCTAssertNotEqualObjects(
+ doc1, [FSTDocument documentWithData:data2 key:key1 version:version1 hasLocalMutations:NO]);
+ XCTAssertNotEqualObjects(
+ doc1, [FSTDocument documentWithData:data1 key:key2 version:version1 hasLocalMutations:NO]);
+ XCTAssertNotEqualObjects(
+ doc1, [FSTDocument documentWithData:data1 key:key1 version:version2 hasLocalMutations:NO]);
+ XCTAssertNotEqualObjects(
+ doc1, [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES]);
+
+ XCTAssertEqualObjects(
+ [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES],
+ [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:5]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.m b/Firestore/Example/Tests/Model/FSTFieldValueTests.m
new file mode 100644
index 0000000..a357e60
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTFieldValueTests.m
@@ -0,0 +1,576 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Model/FSTFieldValue.h"
+
+#import <XCTest/XCTest.h>
+
+#import "API/FIRFirestore+Internal.h"
+#import "API/FSTUserDataConverter.h"
+#import "Core/FSTTimestamp.h"
+#import "Firestore/FIRGeoPoint.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+/** Helper to wrap the values in a set of equality groups using FSTTestFieldValue(). */
+NSArray *FSTWrapGroups(NSArray *groups) {
+ NSMutableArray *wrapped = [NSMutableArray array];
+ for (NSArray<id> *group in groups) {
+ NSMutableArray *wrappedGroup = [NSMutableArray array];
+ for (id value in group) {
+ FSTFieldValue *wrappedValue;
+ // Server Timestamp values can't be parsed directly, so we have a couple predefined sentinel
+ // strings that can be used instead.
+ if ([value isEqual:@"server-timestamp-1"]) {
+ wrappedValue = [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 5, 20, 10, 20, 0)];
+ } else if ([value isEqual:@"server-timestamp-2"]) {
+ wrappedValue = [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 10, 21, 15, 32, 0)];
+ } else if ([value isKindOfClass:[FSTDocumentKeyReference class]]) {
+ // We directly convert these here so that the databaseIDs can be different.
+ FSTDocumentKeyReference *reference = (FSTDocumentKeyReference *)value;
+ wrappedValue =
+ [FSTReferenceValue referenceValue:reference.key databaseID:reference.databaseID];
+ } else {
+ wrappedValue = FSTTestFieldValue(value);
+ }
+ [wrappedGroup addObject:wrappedValue];
+ }
+ [wrapped addObject:wrappedGroup];
+ }
+ return wrapped;
+}
+
+@interface FSTFieldValueTests : XCTestCase
+@end
+
+@implementation FSTFieldValueTests {
+ NSDate *date1;
+ NSDate *date2;
+}
+
+- (void)setUp {
+ [super setUp];
+ // Create a couple date objects for use in tests.
+ date1 = FSTTestDate(2016, 5, 20, 10, 20, 0);
+ date2 = FSTTestDate(2016, 10, 21, 15, 32, 0);
+}
+
+- (void)testWrapIntegers {
+ NSArray *values = @[
+ @(INT_MIN), @(-1), @0, @1, @2, @(UCHAR_MAX), @(INT_MAX), // Standard integers
+ @(LONG_MIN), @(LONG_MAX), @(LLONG_MIN), @(LLONG_MAX) // Larger values
+ ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTIntegerValue class]);
+ XCTAssertEqualObjects([wrapped value], @([value longLongValue]));
+ }
+}
+
+- (void)testWrapsDoubles {
+ // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal
+ // number: <https://en.wikipedia.org/wiki/Denormal_number>.
+ NSArray *values = @[
+ @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * -1.0), @(-1.1), @(-0x1.0p-1074), @(-0.0), @(0.0),
+ @(0x1.0p-1074), @(DBL_MIN), @(1.1), @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY)
+ ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTDoubleValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+}
+
+- (void)testWrapsNilAndNSNull {
+ FSTNullValue *nullValue = [FSTNullValue nullValue];
+ XCTAssertEqual(FSTTestFieldValue(nil), nullValue);
+ XCTAssertEqual(FSTTestFieldValue([NSNull null]), nullValue);
+ XCTAssertEqual([nullValue value], [NSNull null]);
+}
+
+- (void)testWrapsBooleans {
+ NSArray *values = @[ @YES, @NO, [NSNumber numberWithChar:1], [NSNumber numberWithChar:0] ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTBooleanValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+
+ // Unsigned chars could conceivably be handled consistently with signed chars but on arm64 these
+ // end up being stored as signed shorts.
+ FSTFieldValue *wrapped = FSTTestFieldValue([NSNumber numberWithUnsignedChar:1]);
+ XCTAssertEqualObjects(wrapped, [FSTIntegerValue integerValue:1]);
+}
+
+union DoubleBits {
+ double d;
+ uint64_t bits;
+};
+
+- (void)testNormalizesNaNs {
+ // NOTE: With v1beta1 query semantics, it's no longer as important that our NaN representation
+ // matches the backend, since all NaNs are defined to sort as equal, but we preserve the
+ // normalization and this test regardless for now.
+
+ // We use a canonical NaN bit pattern that's common for both Java and Objective-C. Specifically:
+ // - sign: 0
+ // - exponent: 11 bits, all 1
+ // - significand: 52 bits, MSB=1, rest=0
+ //
+ // This matches the Firestore backend which uses Java's Double.doubleToLongBits which is defined
+ // to normalize all NaNs to this value.
+ union DoubleBits canonical = {.bits = 0x7ff8000000000000ULL};
+
+ // IEEE 754 specifies that NaN isn't equal to itself.
+ XCTAssertTrue(isnan(canonical.d));
+ XCTAssertEqual(canonical.bits, canonical.bits);
+ XCTAssertNotEqual(canonical.d, canonical.d);
+
+ // All permutations of the 51 other non-MSB significand bits are also NaNs.
+ union DoubleBits alternate = {.bits = 0x7fff000000000000ULL};
+ XCTAssertTrue(isnan(alternate.d));
+ XCTAssertNotEqual(alternate.bits, canonical.bits);
+ XCTAssertNotEqual(alternate.d, canonical.d);
+
+ // Even though at the C-level assignment preserves non-canonical NaNs, NSNumber normalizes all
+ // NaNs to single shared instance, kCFNumberNaN. That NaN has no public definition for its value
+ // but it happens to match what we need.
+ union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:alternate.d] doubleValue]};
+ XCTAssertEqual(normalized.bits, canonical.bits);
+
+ // Ensure we get the same normalization behavior (currently implemented explicitly by checking
+ // for isnan() and then explicitly assigning NAN).
+ union DoubleBits result;
+ result.d = [[FSTDoubleValue doubleValue:canonical.d] internalValue];
+ XCTAssertEqual(result.bits, canonical.bits);
+
+ result.d = [[FSTDoubleValue doubleValue:alternate.d] internalValue];
+ XCTAssertEqual(result.bits, canonical.bits);
+
+ // A NaN that's canonical except it has the sign bit set (would be negative if signs mattered)
+ union DoubleBits negative = {.bits = 0xfff8000000000000ULL};
+ result.d = [[FSTDoubleValue doubleValue:negative.d] internalValue];
+ XCTAssertTrue(isnan(negative.d));
+ XCTAssertEqual(result.bits, canonical.bits);
+
+ // A signaling NaN with significand where MSB is 0, and some non-MSB bit is one.
+ union DoubleBits signaling = {.bits = 0xfff4000000000000ULL};
+ XCTAssertTrue(isnan(signaling.d));
+ result.d = [[FSTDoubleValue doubleValue:signaling.d] internalValue];
+ XCTAssertEqual(result.bits, canonical.bits);
+}
+
+- (void)testZeros {
+ // Floating point numbers have an explicit sign bit so it's possible to end up with negative
+ // zero as a distinct value from positive zero.
+ union DoubleBits zero = {.d = 0.0};
+ union DoubleBits negativeZero = {.d = -0.0};
+
+ // IEEE 754 requires these two zeros to compare equal.
+ XCTAssertNotEqual(zero.bits, negativeZero.bits);
+ XCTAssertEqual(zero.d, negativeZero.d);
+
+ // NSNumber preserves the negative zero value but compares equal according to IEEE 754.
+ union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:negativeZero.d] doubleValue]};
+ XCTAssertEqual(normalized.bits, negativeZero.bits);
+ XCTAssertEqualObjects([NSNumber numberWithDouble:0.0], [NSNumber numberWithDouble:-0.0]);
+
+ // FSTDoubleValue preserves positive/negative zero
+ union DoubleBits result;
+ result.d = [[[FSTDoubleValue doubleValue:zero.d] value] doubleValue];
+ XCTAssertEqual(result.bits, zero.bits);
+ result.d = [[[FSTDoubleValue doubleValue:negativeZero.d] value] doubleValue];
+ XCTAssertEqual(result.bits, negativeZero.bits);
+
+ // ... but compares positive/negative zero as unequal, compatibly with Firestore.
+ XCTAssertNotEqualObjects([FSTDoubleValue doubleValue:0.0], [FSTDoubleValue doubleValue:-0.0]);
+}
+
+- (void)testWrapStrings {
+ NSArray *values = @[ @"", @"abc" ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTStringValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+}
+
+- (void)testWrapDates {
+ NSArray *values = @[ FSTTestDate(1900, 12, 1, 1, 20, 30), FSTTestDate(2017, 4, 24, 13, 20, 30) ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTTimestampValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+
+ XCTAssertEqualObjects(((FSTTimestampValue *)wrapped).internalValue,
+ [FSTTimestamp timestampWithDate:value]);
+ }
+}
+
+- (void)testWrapGeoPoints {
+ NSArray *values = @[ FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-20, 100) ];
+
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTGeoPointValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+}
+
+- (void)testWrapBlobs {
+ NSArray *values = @[ FSTTestData(1, 2, 3), FSTTestData(1, 2) ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTBlobValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+}
+
+- (void)testWrapResourceNames {
+ NSArray *values = @[
+ FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"),
+ FSTTestRef(@"project", kDefaultDatabaseID, @"foo/baz")
+ ];
+ for (FSTDocumentKeyReference *value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTReferenceValue class]);
+ XCTAssertEqualObjects([wrapped value], value.key);
+ XCTAssertEqualObjects(((FSTDatabaseID *)wrapped).databaseID, value.databaseID);
+ }
+}
+
+- (void)testWrapsEmptyObjects {
+ XCTAssertEqualObjects(FSTTestFieldValue(@{}), [FSTObjectValue objectValue]);
+}
+
+- (void)testWrapsSimpleObjects {
+ FSTObjectValue *actual = FSTTestObjectValue(
+ @{ @"a" : @"foo",
+ @"b" : @(1L),
+ @"c" : @YES,
+ @"d" : [NSNull null] });
+ FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{
+ @"a" : [FSTStringValue stringValue:@"foo"],
+ @"b" : [FSTIntegerValue integerValue:1LL],
+ @"c" : [FSTBooleanValue trueValue],
+ @"d" : [FSTNullValue nullValue]
+ }];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testWrapsNestedObjects {
+ FSTObjectValue *actual = FSTTestObjectValue(@{ @"a" : @{@"b" : @{@"c" : @"foo"}, @"d" : @YES} });
+ FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{
+ @"a" : [[FSTObjectValue alloc] initWithDictionary:@{
+ @"b" :
+ [[FSTObjectValue alloc] initWithDictionary:@{@"c" : [FSTStringValue stringValue:@"foo"]}],
+ @"d" : [FSTBooleanValue booleanValue:YES]
+ }]
+ }];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testExtractsFields {
+ FSTObjectValue *obj = FSTTestObjectValue(@{ @"foo" : @{@"a" : @YES, @"b" : @"string"} });
+ FSTAssertIsKindOfClass(obj, FSTObjectValue);
+
+ FSTAssertIsKindOfClass([obj valueForPath:FSTTestFieldPath(@"foo")], FSTObjectValue);
+ XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.a")], [FSTBooleanValue trueValue]);
+ XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.b")],
+ [FSTStringValue stringValue:@"string"]);
+
+ XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"foo.a.b")]);
+ XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar")]);
+ XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar.a")]);
+}
+
+- (void)testOverwritesExistingFields {
+ FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"});
+ FSTObjectValue *mod =
+ [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")];
+
+ // Should return a new object, leaving the old one unmodified.
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"}));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"}));
+}
+
+- (void)testAddsNewFields {
+ FSTObjectValue *empty = [FSTObjectValue objectValue];
+ FSTObjectValue *mod =
+ [empty objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")];
+ XCTAssertNotEqual(empty, mod);
+ XCTAssertEqualObjects(empty, FSTTestFieldValue(@{}));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"}));
+
+ FSTObjectValue *old = mod;
+ mod = [old objectBySettingValue:FSTTestFieldValue(@1) forPath:FSTTestFieldPath(@"b")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"mod"}));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @"mod", @"b" : @1 }));
+}
+
+- (void)testImplicitlyCreatesObjects {
+ FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"});
+ FSTObjectValue *mod =
+ [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"b.c.d")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"}));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(
+ @{ @"a" : @"old",
+ @"b" : @{@"c" : @{@"d" : @"mod"}} }));
+}
+
+- (void)testCanOverwritePrimitivesWithObjects {
+ FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} });
+ FSTObjectValue *mod =
+ [old objectBySettingValue:FSTTestFieldValue(@{@"b" : @"mod"}) forPath:FSTTestFieldPath(@"a")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"mod"} }));
+}
+
+- (void)testAddsToNestedObjects {
+ FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} });
+ FSTObjectValue *mod =
+ [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a.c")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old", @"c" : @"mod"} }));
+}
+
+- (void)testDeletesKeys {
+ FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @1, @"b" : @2 });
+ FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @1, @"b" : @2 }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 }));
+
+ FSTObjectValue *empty = [mod objectByDeletingPath:FSTTestFieldPath(@"b")];
+ XCTAssertNotEqual(mod, empty);
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 }));
+ XCTAssertEqualObjects(empty, FSTTestFieldValue(@{}));
+}
+
+- (void)testDeletesHandleMissingKeys {
+ FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @1, @"c" : @2} });
+ FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"b")];
+ XCTAssertEqualObjects(old, mod);
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }));
+
+ mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.d")];
+ XCTAssertEqualObjects(old, mod);
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }));
+
+ mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.b.c")];
+ XCTAssertEqualObjects(old, mod);
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }));
+}
+
+- (void)testDeletesNestedKeys {
+ FSTObjectValue *old = FSTTestObjectValue(
+ @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} });
+ FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c.d")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(
+ @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} }));
+
+ old = mod;
+ mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c")];
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} }));
+
+ old = mod;
+ mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")];
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{}));
+}
+
+- (void)testArrays {
+ FSTArrayValue *expected = [[FSTArrayValue alloc]
+ initWithValueNoCopy:@[ [FSTStringValue stringValue:@"value"], [FSTBooleanValue trueValue] ]];
+
+ FSTArrayValue *actual = (FSTArrayValue *)FSTTestFieldValue(@[ @"value", @YES ]);
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testValueEquality {
+ NSArray *groups = @[
+ @[ FSTTestFieldValue(@YES), [FSTBooleanValue booleanValue:YES] ],
+ @[ FSTTestFieldValue(@NO), [FSTBooleanValue booleanValue:NO] ],
+ @[ FSTTestFieldValue([NSNull null]), [FSTNullValue nullValue] ],
+ @[ FSTTestFieldValue(@(0.0 / 0.0)), FSTTestFieldValue(@(NAN)), [FSTDoubleValue nanValue] ],
+ // -0.0 and 0.0 compare: the same (but are not isEqual:)
+ @[ FSTTestFieldValue(@(-0.0)) ], @[ FSTTestFieldValue(@0.0) ],
+ @[ FSTTestFieldValue(@1), FSTTestFieldValue(@1LL), [FSTIntegerValue integerValue:1LL] ],
+ // double and unit64_t values can compare: the same (but won't be isEqual:)
+ @[ FSTTestFieldValue(@1.0), [FSTDoubleValue doubleValue:1.0] ],
+ @[ FSTTestFieldValue(@1.1), [FSTDoubleValue doubleValue:1.1] ],
+ @[
+ FSTTestFieldValue(FSTTestData(0, 1, 2, -1)), [FSTBlobValue blobValue:FSTTestData(0, 1, 2, -1)]
+ ],
+ @[ FSTTestFieldValue(FSTTestData(0, 1, -1)) ],
+ @[ FSTTestFieldValue(@"string"), [FSTStringValue stringValue:@"string"] ],
+ @[ FSTTestFieldValue(@"strin") ],
+ @[ FSTTestFieldValue(@"e\u0301b") ], // latin small letter e + combining acute accent
+ @[ FSTTestFieldValue(@"\u00e9a") ], // latin small letter e with acute accent
+ @[
+ FSTTestFieldValue(date1),
+ [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:date1]]
+ ],
+ @[ FSTTestFieldValue(date2) ],
+ @[
+ // NOTE: ServerTimestampValues can't be parsed via FSTTestFieldValue().
+ [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]],
+ [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]]
+ ],
+ @[ [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date2]] ],
+ @[
+ FSTTestFieldValue(FSTTestGeoPoint(0, 1)),
+ [FSTGeoPointValue geoPointValue:FSTTestGeoPoint(0, 1)]
+ ],
+ @[ FSTTestFieldValue(FSTTestGeoPoint(1, 0)) ],
+ @[
+ [FSTReferenceValue referenceValue:FSTTestDocKey(@"coll/doc1")
+ databaseID:[FSTDatabaseID databaseIDWithProject:@"project"
+ database:kDefaultDatabaseID]],
+ FSTTestFieldValue(FSTTestRef(@"project", kDefaultDatabaseID, @"coll/doc1"))
+ ],
+ @[ FSTTestRef(@"project", @"(default)", @"coll/doc2") ],
+ @[ FSTTestFieldValue(@[ @"foo", @"bar" ]), FSTTestFieldValue(@[ @"foo", @"bar" ]) ],
+ @[ FSTTestFieldValue(@[ @"foo", @"bar", @"baz" ]) ], @[ FSTTestFieldValue(@[ @"foo" ]) ],
+ @[
+ FSTTestFieldValue(
+ @{ @"bar" : @1,
+ @"foo" : @2 }),
+ FSTTestFieldValue(
+ @{ @"foo" : @2,
+ @"bar" : @1 })
+ ],
+ @[ FSTTestFieldValue(
+ @{ @"bar" : @2,
+ @"foo" : @1 }) ],
+ @[ FSTTestFieldValue(
+ @{ @"bar" : @1,
+ @"foo" : @1 }) ],
+ @[ FSTTestFieldValue(
+ @{ @"foo" : @1 }) ]
+ ];
+
+ FSTAssertEqualityGroups(groups);
+}
+
+- (void)testValueOrdering {
+ NSArray *groups = @[
+ // null first
+ @[ [NSNull null] ],
+
+ // booleans
+ @[ @NO ], @[ @YES ],
+
+ // numbers
+ @[ @(0.0 / 0.0) ], @[ @(-INFINITY) ], @[ @(-DBL_MAX) ], @[ @(LLONG_MIN) ], @[ @(-1.1) ],
+ @[ @(-1.0), @(-1LL) ], // longs and doubles compare the same
+ @[ @(-DBL_MIN) ],
+ @[ @(-0x1.0p-1074) ], // negative smallest subnormal
+ @[ @(-0.0), @(0.0), @(0LL) ], // zeros all compare the same
+ @[ @(0x1.0p-1074) ], // positive smallest subnormal
+ @[ @(DBL_MIN) ], @[ @1.0, @1LL ], // longs and doubles compare the same
+ @[ @1.1 ], @[ @(LLONG_MAX) ], @[ @(DBL_MAX) ], @[ @(INFINITY) ],
+
+ // timestamps
+ @[ date1 ], @[ date2 ],
+
+ // server timestamps come after all concrete timestamps.
+ // NOTE: server timestamps can't be parsed directly, so we have special sentinel strings (see
+ // FSTWrapGroups()).
+ @[ @"server-timestamp-1" ], @[ @"server-timestamp-2" ],
+
+ // strings
+ @[ @"" ], @[ @"\000\ud7ff\ue000\uffff" ], @[ @"(╯°□°)╯︵ ┻━┻" ], @[ @"a" ], @[ @"abc def" ],
+ @[ @"e\u0301b" ], // latin small letter e + combining acute accent + latin small letter b
+ @[ @"æ" ],
+ @[ @"\u00e9a" ], // latin small letter e with acute accent + latin small letter a
+
+ // blobs
+ @[ FSTTestData(-1) ], @[ FSTTestData(0, -1) ], @[ FSTTestData(0, 1, 2, 3, 4, -1) ],
+ @[ FSTTestData(0, 1, 2, 4, 3, -1) ], @[ FSTTestData(255, -1) ],
+
+ // resource names
+ @[ FSTTestRef(@"p1", @"d1", @"c1/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c1/doc2") ],
+ @[ FSTTestRef(@"p1", @"d1", @"c10/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c2/doc1") ],
+ @[ FSTTestRef(@"p1", @"d2", @"c1/doc1") ], @[ FSTTestRef(@"p2", @"d1", @"c1/doc1") ],
+
+ // Geo points
+ @[ FSTTestGeoPoint(-90, -180) ], @[ FSTTestGeoPoint(-90, 0) ], @[ FSTTestGeoPoint(-90, 180) ],
+ @[ FSTTestGeoPoint(0, -180) ], @[ FSTTestGeoPoint(0, 0) ], @[ FSTTestGeoPoint(0, 180) ],
+ @[ FSTTestGeoPoint(1, -180) ], @[ FSTTestGeoPoint(1, 0) ], @[ FSTTestGeoPoint(1, 180) ],
+ @[ FSTTestGeoPoint(90, -180) ], @[ FSTTestGeoPoint(90, 0) ], @[ FSTTestGeoPoint(90, 180) ],
+
+ // Arrays
+ @[ @[] ], @[ @[ @"bar" ] ], @[ @[ @"foo" ] ], @[ @[ @"foo", @1 ] ], @[ @[ @"foo", @2 ] ],
+ @[ @[ @"foo", @"0" ] ],
+
+ // Objects
+ @[
+ @{ @"bar" : @0 }
+ ],
+ @[
+ @{ @"bar" : @0,
+ @"foo" : @1 }
+ ],
+ @[
+ @{ @"foo" : @1 }
+ ],
+ @[
+ @{ @"foo" : @2 }
+ ],
+ @[ @{@"foo" : @"0"} ]
+ ];
+
+ NSArray *wrapped = FSTWrapGroups(groups);
+ FSTAssertComparisons(wrapped);
+}
+
+- (void)testValue {
+ NSDate *date = [NSDate date];
+ id input = @{ @"array" : @[ @1, date ], @"obj" : @{@"date" : date, @"string" : @"hi"} };
+ FSTObjectValue *value = FSTTestObjectValue(input);
+ id output = [value value];
+ {
+ XCTAssertTrue([output[@"array"][1] isKindOfClass:[NSDate class]]);
+ NSDate *actual = output[@"array"][1];
+ XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970,
+ 0.000000001);
+ }
+ {
+ XCTAssertTrue([output[@"obj"][@"date"] isKindOfClass:[NSDate class]]);
+ NSDate *actual = output[@"obj"][@"date"];
+ XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970,
+ 0.000000001);
+ }
+}
+
+@end
diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.m b/Firestore/Example/Tests/Model/FSTMutationTests.m
new file mode 100644
index 0000000..5ad9f94
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTMutationTests.m
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Model/FSTMutation.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTTimestamp.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+@interface FSTMutationTests : XCTestCase
+@end
+
+@implementation FSTMutationTests {
+ FSTTimestamp *_timestamp;
+}
+
+- (void)setUp {
+ _timestamp = [FSTTimestamp timestamp];
+}
+
+- (void)testAppliesSetsToDocuments {
+ NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"bar" : @"bar-value"});
+ FSTMaybeDocument *setDoc = [set applyTo:baseDoc localWriteTime:_timestamp];
+
+ NSDictionary *expectedData = @{@"bar" : @"bar-value"};
+ XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES));
+}
+
+- (void)testAppliesPatchesToDocuments {
+ NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" };
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *patch =
+ FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil);
+ FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp];
+
+ NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" };
+ XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES));
+}
+
+- (void)testDeletesValuesFromTheFieldMask {
+ NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value", @"baz" : @"baz-value"} };
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"collection/key"];
+ FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"foo.bar") ]];
+ FSTMutation *patch = [[FSTPatchMutation alloc] initWithKey:key
+ fieldMask:mask
+ value:[FSTObjectValue objectValue]
+ precondition:[FSTPrecondition none]];
+ FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp];
+
+ NSDictionary *expectedData = @{ @"foo" : @{@"baz" : @"baz-value"} };
+ XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES));
+}
+
+- (void)testPatchesPrimitiveValue {
+ NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *patch =
+ FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil);
+ FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp];
+
+ NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" };
+ XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES));
+}
+
+- (void)testPatchingDeletedDocumentsDoesNothing {
+ FSTMaybeDocument *baseDoc = FSTTestDeletedDoc(@"collection/key", 0);
+ FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"bar"}, nil);
+ FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp];
+ XCTAssertEqualObjects(patchedDoc, baseDoc);
+}
+
+- (void)testAppliesLocalTransformsToDocuments {
+ NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" };
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]);
+ FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc localWriteTime:_timestamp];
+
+ // Server timestamps aren't parsed, so we manually insert it.
+ FSTObjectValue *expectedData = FSTTestObjectValue(
+ @{ @"foo" : @{@"bar" : @"<server-timestamp>"},
+ @"baz" : @"baz-value" });
+ expectedData =
+ [expectedData objectBySettingValue:[FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:_timestamp]
+ forPath:FSTTestFieldPath(@"foo.bar")];
+
+ FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData
+ key:FSTTestDocKey(@"collection/key")
+ version:FSTTestVersion(0)
+ hasLocalMutations:YES];
+
+ XCTAssertEqualObjects(transformedDoc, expectedDoc);
+}
+
+- (void)testAppliesServerAckedTransformsToDocuments {
+ NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" };
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]);
+
+ FSTMutationResult *mutationResult = [[FSTMutationResult alloc]
+ initWithVersion:FSTTestVersion(1)
+ transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]];
+
+ FSTMaybeDocument *transformedDoc =
+ [transform applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult];
+
+ NSDictionary *expectedData =
+ @{ @"foo" : @{@"bar" : _timestamp.approximateDateValue},
+ @"baz" : @"baz-value" };
+ XCTAssertEqualObjects(transformedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO));
+}
+
+- (void)testDeleteDeletes {
+ NSDictionary *docData = @{@"foo" : @"bar"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *mutation = FSTTestDeleteMutation(@"collection/key");
+ FSTMaybeDocument *result = [mutation applyTo:baseDoc localWriteTime:_timestamp];
+ XCTAssertEqualObjects(result, FSTTestDeletedDoc(@"collection/key", 0));
+}
+
+- (void)testSetWithMutationResult {
+ NSDictionary *docData = @{@"foo" : @"bar"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"});
+ FSTMutationResult *mutationResult =
+ [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil];
+ FSTMaybeDocument *setDoc =
+ [set applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult];
+
+ NSDictionary *expectedData = @{@"foo" : @"new-bar"};
+ XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO));
+}
+
+- (void)testPatchWithMutationResult {
+ NSDictionary *docData = @{@"foo" : @"bar"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"new-bar"}, nil);
+ FSTMutationResult *mutationResult =
+ [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil];
+ FSTMaybeDocument *patchedDoc =
+ [patch applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult];
+
+ NSDictionary *expectedData = @{@"foo" : @"new-bar"};
+ XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO));
+}
+
+#define ASSERT_VERSION_TRANSITION(mutation, base, expected) \
+ do { \
+ FSTMutationResult *mutationResult = \
+ [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(0) transformResults:nil]; \
+ FSTMaybeDocument *actual = \
+ [mutation applyTo:base localWriteTime:_timestamp mutationResult:mutationResult]; \
+ XCTAssertEqualObjects(actual, expected); \
+ } while (0);
+
+/**
+ * Tests the transition table documented in FSTMutation.h.
+ */
+- (void)testTransitions {
+ FSTDocument *docV0 = FSTTestDoc(@"collection/key", 0, @{}, NO);
+ FSTDeletedDocument *deletedV0 = FSTTestDeletedDoc(@"collection/key", 0);
+
+ FSTDocument *docV3 = FSTTestDoc(@"collection/key", 3, @{}, NO);
+ FSTDeletedDocument *deletedV3 = FSTTestDeletedDoc(@"collection/key", 3);
+
+ FSTMutation *setMutation = FSTTestSetMutation(@"collection/key", @{});
+ FSTMutation *patchMutation = FSTTestPatchMutation(@"collection/key", @{}, nil);
+ FSTMutation *deleteMutation = FSTTestDeleteMutation(@"collection/key");
+
+ ASSERT_VERSION_TRANSITION(setMutation, docV3, docV3);
+ ASSERT_VERSION_TRANSITION(setMutation, deletedV3, docV0);
+ ASSERT_VERSION_TRANSITION(setMutation, nil, docV0);
+
+ ASSERT_VERSION_TRANSITION(patchMutation, docV3, docV3);
+ ASSERT_VERSION_TRANSITION(patchMutation, deletedV3, deletedV3);
+ ASSERT_VERSION_TRANSITION(patchMutation, nil, nil);
+
+ ASSERT_VERSION_TRANSITION(deleteMutation, docV3, deletedV0);
+ ASSERT_VERSION_TRANSITION(deleteMutation, deletedV3, deletedV0);
+ ASSERT_VERSION_TRANSITION(deleteMutation, nil, deletedV0);
+}
+
+#undef ASSERT_TRANSITION
+
+@end
diff --git a/Firestore/Example/Tests/Model/FSTPathTests.m b/Firestore/Example/Tests/Model/FSTPathTests.m
new file mode 100644
index 0000000..a5d3d80
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTPathTests.m
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTHelpers.h"
+#import "Model/FSTPath.h"
+
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTFieldPathTests : XCTestCase
+@end
+
+@implementation FSTFieldPathTests
+
+- (void)testConstructor {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ XCTAssertEqual(3, path.length);
+}
+
+- (void)testIndexing {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ XCTAssertEqualObjects(@"rooms", path.firstSegment);
+ XCTAssertEqualObjects(@"rooms", [path segmentAtIndex:0]);
+ XCTAssertEqualObjects(@"rooms", path[0]);
+
+ XCTAssertEqualObjects(@"Eros", [path segmentAtIndex:1]);
+ XCTAssertEqualObjects(@"Eros", path[1]);
+
+ XCTAssertEqualObjects(@"messages", [path segmentAtIndex:2]);
+ XCTAssertEqualObjects(@"messages", path[2]);
+ XCTAssertEqualObjects(@"messages", path.lastSegment);
+}
+
+- (void)testPathByRemovingFirstSegment {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"Eros", @"messages" ]];
+ FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"messages" ]];
+ FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]];
+
+ XCTAssertEqualObjects(second, path.pathByRemovingFirstSegment);
+ XCTAssertEqualObjects(third, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment);
+ XCTAssertEqualObjects(
+ empty, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment.pathByRemovingFirstSegment);
+ // unmodified original
+ XCTAssertEqualObjects(same, path);
+}
+
+- (void)testPathByRemovingLastSegment {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros" ]];
+ FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"rooms" ]];
+ FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]];
+
+ XCTAssertEqualObjects(second, path.pathByRemovingLastSegment);
+ XCTAssertEqualObjects(third, path.pathByRemovingLastSegment.pathByRemovingLastSegment);
+ XCTAssertEqualObjects(
+ empty, path.pathByRemovingLastSegment.pathByRemovingLastSegment.pathByRemovingLastSegment);
+ // unmodified original
+ XCTAssertEqualObjects(same, path);
+}
+
+- (void)testPathByAppendingSegment {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms" ]];
+ FSTFieldPath *rooms = [FSTFieldPath pathWithSegments:@[ @"rooms" ]];
+ FSTFieldPath *roomsEros = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros" ]];
+ FSTFieldPath *roomsEros1 = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros", @"1" ]];
+
+ XCTAssertEqualObjects(roomsEros, [path pathByAppendingSegment:@"eros"]);
+ XCTAssertEqualObjects(roomsEros1,
+ [[path pathByAppendingSegment:@"eros"] pathByAppendingSegment:@"1"]);
+ // unmodified original
+ XCTAssertEqualObjects(rooms, path);
+
+ FSTFieldPath *sub = [FSTTestFieldPath(@"rooms.eros.1") pathByRemovingFirstSegment];
+ FSTFieldPath *appended = [sub pathByAppendingSegment:@"2"];
+ XCTAssertEqualObjects(appended, FSTTestFieldPath(@"eros.1.2"));
+}
+
+- (void)testPathComparison {
+ FSTFieldPath *path1 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]];
+ FSTFieldPath *path2 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]];
+ FSTFieldPath *path3 = [FSTFieldPath pathWithSegments:@[ @"x", @"y", @"z" ]];
+ XCTAssertTrue([path1 isEqual:path2]);
+ XCTAssertFalse([path1 isEqual:path3]);
+
+ FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]];
+ FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]];
+ FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]];
+ FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]];
+
+ XCTAssertEqual(NSOrderedAscending, [empty compare:a]);
+ XCTAssertEqual(NSOrderedAscending, [a compare:b]);
+ XCTAssertEqual(NSOrderedAscending, [a compare:ab]);
+
+ XCTAssertEqual(NSOrderedDescending, [a compare:empty]);
+ XCTAssertEqual(NSOrderedDescending, [b compare:a]);
+ XCTAssertEqual(NSOrderedDescending, [ab compare:a]);
+}
+
+- (void)testIsPrefixOfPath {
+ FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]];
+ FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]];
+ FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]];
+ FSTFieldPath *abc = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]];
+ FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]];
+ FSTFieldPath *ba = [FSTFieldPath pathWithSegments:@[ @"b", @"a" ]];
+
+ XCTAssertTrue([empty isPrefixOfPath:a]);
+ XCTAssertTrue([empty isPrefixOfPath:ab]);
+ XCTAssertTrue([empty isPrefixOfPath:abc]);
+ XCTAssertTrue([empty isPrefixOfPath:empty]);
+ XCTAssertTrue([empty isPrefixOfPath:b]);
+ XCTAssertTrue([empty isPrefixOfPath:ba]);
+
+ XCTAssertTrue([a isPrefixOfPath:a]);
+ XCTAssertTrue([a isPrefixOfPath:ab]);
+ XCTAssertTrue([a isPrefixOfPath:abc]);
+ XCTAssertFalse([a isPrefixOfPath:empty]);
+ XCTAssertFalse([a isPrefixOfPath:b]);
+ XCTAssertFalse([a isPrefixOfPath:ba]);
+
+ XCTAssertFalse([ab isPrefixOfPath:a]);
+ XCTAssertTrue([ab isPrefixOfPath:ab]);
+ XCTAssertTrue([ab isPrefixOfPath:abc]);
+ XCTAssertFalse([ab isPrefixOfPath:empty]);
+ XCTAssertFalse([ab isPrefixOfPath:b]);
+ XCTAssertFalse([ab isPrefixOfPath:ba]);
+
+ XCTAssertFalse([abc isPrefixOfPath:a]);
+ XCTAssertFalse([abc isPrefixOfPath:ab]);
+ XCTAssertTrue([abc isPrefixOfPath:abc]);
+ XCTAssertFalse([abc isPrefixOfPath:empty]);
+ XCTAssertFalse([abc isPrefixOfPath:b]);
+ XCTAssertFalse([abc isPrefixOfPath:ba]);
+}
+
+- (void)testInvalidPaths {
+ XCTAssertThrows(FSTTestFieldPath(@""));
+ XCTAssertThrows(FSTTestFieldPath(@"."));
+ XCTAssertThrows(FSTTestFieldPath(@".foo"));
+ XCTAssertThrows(FSTTestFieldPath(@"foo."));
+ XCTAssertThrows(FSTTestFieldPath(@"foo..bar"));
+}
+
+#define ASSERT_ROUND_TRIP(str, segments) \
+ do { \
+ FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:str]; \
+ XCTAssertEqual([path length], segments); \
+ NSString *canonical = [path canonicalString]; \
+ XCTAssertEqualObjects(canonical, str); \
+ } while (0);
+
+- (void)testCanonicalString {
+ ASSERT_ROUND_TRIP(@"foo", 1);
+ ASSERT_ROUND_TRIP(@"foo.bar", 2);
+ ASSERT_ROUND_TRIP(@"foo.bar.baz", 3);
+ ASSERT_ROUND_TRIP(@"`.foo\\\\`", 1);
+ ASSERT_ROUND_TRIP(@"`.foo\\\\`.`.foo`", 2);
+ ASSERT_ROUND_TRIP(@"foo.`\\``.bar", 3);
+}
+
+#undef ASSERT_ROUND_TRIP
+
+- (void)testCanonicalStringOfSubstring {
+ FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:@"foo.bar.baz"];
+ XCTAssertEqualObjects([path canonicalString], @"foo.bar.baz");
+
+ FSTFieldPath *pathTail = [path pathByRemovingFirstSegment];
+ XCTAssertEqualObjects([pathTail canonicalString], @"bar.baz");
+
+ FSTFieldPath *pathHead = [path pathByRemovingLastSegment];
+ XCTAssertEqualObjects([pathHead canonicalString], @"foo.bar");
+
+ XCTAssertEqualObjects([[pathTail pathByRemovingLastSegment] canonicalString], @"bar");
+ XCTAssertEqualObjects([[pathHead pathByRemovingFirstSegment] canonicalString], @"bar");
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTDatastoreTests.m b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m
new file mode 100644
index 0000000..511de72
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTDatastoreTests.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 "Firestore/FIRFirestoreErrors.h"
+#import "Remote/FSTDatastore.h"
+
+#import <GRPCClient/GRPCCall.h>
+#import <XCTest/XCTest.h>
+
+@interface FSTDatastoreTests : XCTestCase
+@end
+
+@implementation FSTDatastoreTests
+
+- (void)testIsPermanentWriteError {
+ // From GRPCCall -cancel
+ NSError *error = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeCancelled
+ userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}];
+ XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
+
+ // From GRPCCall -startNextRead
+ error =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeResourceExhausted
+ userInfo:@{
+ NSLocalizedDescriptionKey :
+ @"Client does not have enough memory to hold the server response."
+ }];
+ XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
+
+ // From GRPCCall -startWithWriteable
+ error = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeUnavailable
+ userInfo:@{NSLocalizedDescriptionKey : @"Connectivity lost."}];
+ XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
+
+ // User info doesn't matter:
+ error = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeUnavailable
+ userInfo:nil];
+ XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m
new file mode 100644
index 0000000..a172af7
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m
@@ -0,0 +1,556 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Remote/FSTRemoteEvent.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Remote/FSTExistenceFilter.h"
+#import "Remote/FSTWatchChange.h"
+
+#import "FSTHelpers.h"
+#import "FSTWatchChange+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTRemoteEventTests : XCTestCase
+@end
+
+@implementation FSTRemoteEventTests {
+ NSData *_resumeToken1;
+ NSMutableDictionary<NSNumber *, NSNumber *> *_noPendingResponses;
+}
+
+- (void)setUp {
+ _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding];
+ _noPendingResponses = [NSMutableDictionary dictionary];
+}
+
+- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray<NSNumber *> *)targets
+ outstanding:
+ (NSDictionary<NSNumber *, NSNumber *> *)outstanding
+ changes:(NSArray<FSTWatchChange *> *)watchChanges {
+ NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
+ FSTQueryData *dummyQueryData = [FSTQueryData alloc];
+ for (NSNumber *targetID in targets) {
+ listens[targetID] = dummyQueryData;
+ }
+ FSTWatchChangeAggregator *aggregator =
+ [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(3)
+ listenTargets:listens
+ pendingTargetResponses:outstanding];
+ [aggregator addWatchChanges:watchChanges];
+ return aggregator;
+}
+
+- (void)testWillAccumulateDocumentAddedAndRemovedEvents {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ]
+ removedTargetIDs:@[ @4, @5, @6 ]
+ documentKey:doc1.key
+ document:doc1];
+
+ FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @4 ]
+ removedTargetIDs:@[ @2, @6 ]
+ documentKey:doc2.key
+ document:doc2];
+
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2, @3, @4, @5, @6 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 2);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+
+ XCTAssertEqual(event.targetChanges.count, 6);
+
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+
+ FSTUpdateMapping *mapping2 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]];
+ XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2);
+
+ FSTUpdateMapping *mapping3 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3);
+
+ FSTUpdateMapping *mapping4 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]];
+ XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4);
+
+ FSTUpdateMapping *mapping5 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]];
+ XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5);
+
+ FSTUpdateMapping *mapping6 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]];
+ XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6);
+}
+
+- (void)testWillIgnoreEventsForPendingTargets {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+
+ FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc2.key
+ document:doc2];
+
+ // We're waiting for the unwatch and watch ack
+ NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2 };
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ]
+ outstanding:pendingResponses
+ changes:@[ change1, change2, change3, change4 ]];
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ // doc1 is ignored because it was part of an inactive target, but doc2 is in the changes
+ // because it become active.
+ XCTAssertEqual(event.documentUpdates.count, 1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+
+ XCTAssertEqual(event.targetChanges.count, 1);
+}
+
+- (void)testWillIgnoreEventsForRemovedTargets {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+
+ FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ // We're waiting for the unwatch ack
+ NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @1 };
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ // doc1 is ignored because it was part of an inactive target
+ XCTAssertEqual(event.documentUpdates.count, 0);
+
+ // Target 1 is ignored because it was removed
+ XCTAssertEqual(event.targetChanges.count, 0);
+}
+
+- (void)testWillKeepResetMappingEvenWithUpdates {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+ // Reset stream, ignoring doc1
+ FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ // Add doc2, doc3
+ FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc2.key
+ document:doc2];
+ FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc3.key
+ document:doc3];
+
+ // Remove doc2 again, should not show up in reset mapping
+ FSTWatchChange *change5 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1 ]
+ documentKey:doc2.key
+ document:doc2];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2, change3, change4, change5 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 3);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+ XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3);
+
+ XCTAssertEqual(event.targetChanges.count, 1);
+
+ // Only doc3 is part of the new mapping
+ FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]];
+
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping);
+}
+
+- (void)testWillHandleSingleReset {
+ // Reset target
+ FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 0);
+
+ XCTAssertEqual(event.targetChanges.count, 1);
+
+ // Reset mapping is empty
+ FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping);
+}
+
+- (void)testWillHandleTargetAddAndRemovalInSameBatch {
+ FSTDocument *doc1a = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc1b = FSTTestDoc(@"docs/1", 1, @{ @"value" : @2 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[ @2 ]
+ documentKey:doc1a.key
+ document:doc1a];
+
+ FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ]
+ removedTargetIDs:@[ @1 ]
+ documentKey:doc1b.key
+ document:doc1b];
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 1);
+ XCTAssertEqualObjects(event.documentUpdates[doc1b.key], doc1b);
+
+ XCTAssertEqual(event.targetChanges.count, 2);
+
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+
+ FSTUpdateMapping *mapping2 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2);
+}
+
+- (void)testTargetCurrentChangeWillMarkTheTargetCurrent {
+ FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 0);
+ XCTAssertEqual(event.targetChanges.count, 1);
+ FSTTargetChange *targetChange = event.targetChanges[@1];
+ XCTAssertEqualObjects(targetChange.mapping, [[FSTUpdateMapping alloc] init]);
+ XCTAssertEqual(targetChange.currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(targetChange.resumeToken, _resumeToken1);
+}
+
+- (void)testTargetAddedChangeWillResetPreviousState {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @3 ]
+ removedTargetIDs:@[ @2 ]
+ documentKey:doc1.key
+ document:doc1];
+ FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1, @2, @3 ]
+ resumeToken:_resumeToken1];
+ FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:@[ @1 ]
+ cause:nil];
+ FSTWatchChange *change4 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:@[ @2 ]
+ cause:nil];
+ FSTWatchChange *change5 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded
+ targetIDs:@[ @1 ]
+ cause:nil];
+ FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[ @3 ]
+ documentKey:doc2.key
+ document:doc2];
+
+ NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2, @2 : @1 };
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1, @3 ]
+ outstanding:pendingResponses
+ changes:@[ change1, change2, change3, change4, change5, change6 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 2);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+
+ // target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove
+ XCTAssertEqual(event.targetChanges.count, 2);
+
+ // doc1 was before the remove, so it does not show up in the mapping
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ // Current was before the remove
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone);
+
+ // Doc1 was before the remove
+ FSTUpdateMapping *mapping3 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]];
+ XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3);
+ // Current was before the remove
+ XCTAssertEqual(event.targetChanges[@3].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(event.targetChanges[@3].resumeToken, _resumeToken1);
+}
+
+- (void)testNoChangeWillStillMarkTheAffectedTargets {
+ FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 0);
+ XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]);
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone);
+ XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
+}
+
+- (void)testExistenceFiltersWillReplacePreviousExistenceFilters {
+ FSTExistenceFilter *filter1 = [FSTExistenceFilter filterWithCount:1];
+ FSTExistenceFilter *filter2 = [FSTExistenceFilter filterWithCount:2];
+ FSTWatchChange *change1 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:1];
+ FSTWatchChange *change2 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:2];
+ // replace filter1 for target 2
+ FSTWatchChange *change3 = [FSTExistenceFilterWatchChange changeWithFilter:filter2 targetID:2];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1, @2 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2, change3 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 0);
+ XCTAssertEqual(event.targetChanges.count, 0);
+ XCTAssertEqual(aggregator.existenceFilters.count, 2);
+ XCTAssertEqual(aggregator.existenceFilters[@1], filter1);
+ XCTAssertEqual(aggregator.existenceFilters[@2], filter2);
+}
+
+- (void)testExistenceFilterMismatchResetsTarget {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+
+ FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc2.key
+ document:doc2];
+
+ FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2, change3 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 2);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+
+ XCTAssertEqual(event.targetChanges.count, 1);
+
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
+
+ [event handleExistenceFilterMismatchForTargetID:@1];
+
+ // Mapping is reset
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]);
+ // Reset the resume snapshot
+ XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(0));
+ // Target needs to be set to not current
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent);
+ XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0);
+}
+
+- (void)testDocumentUpdate {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDeletedDocument *deletedDoc1 =
+ [FSTDeletedDocument documentWithKey:doc1.key version:FSTTestVersion(3)];
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+
+ FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc2.key
+ document:doc2];
+
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 2);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+
+ // Update doc1
+ [event addDocumentUpdate:deletedDoc1];
+ [event addDocumentUpdate:doc3];
+
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 3);
+ // doc1 is replaced
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], deletedDoc1);
+ // doc2 is untouched
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+ // doc3 is new
+ XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3);
+
+ // Target is unchanged
+ XCTAssertEqual(event.targetChanges.count, 1);
+
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+}
+
+- (void)testResumeTokensHandledPerTarget {
+ NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding];
+ FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @2 ]
+ resumeToken:resumeToken2];
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqual(event.targetChanges.count, 2);
+
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
+
+ XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1);
+ XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2);
+}
+
+- (void)testLastResumeTokenWins {
+ NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding];
+ NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding];
+
+ FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @1 ]
+ resumeToken:resumeToken2];
+ FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @2 ]
+ resumeToken:resumeToken3];
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1, @2 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2, change3 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqual(event.targetChanges.count, 2);
+
+ FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2);
+
+ XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1);
+ XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone);
+ XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m
new file mode 100644
index 0000000..c4cf9df
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m
@@ -0,0 +1,794 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Remote/FSTSerializerBeta.h"
+
+#import <GRPCClient/GRPCCall.h>
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTTimestamp.h"
+#import "Firestore/FIRFieldPath.h"
+#import "Firestore/FIRFirestoreErrors.h"
+#import "Firestore/FIRGeoPoint.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+#import "Model/FSTPath.h"
+#import "Protos/objc/firestore/local/MaybeDocument.pbobjc.h"
+#import "Protos/objc/firestore/local/Mutation.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Common.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Document.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Query.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Write.pbobjc.h"
+#import "Protos/objc/google/rpc/Status.pbobjc.h"
+#import "Protos/objc/google/type/Latlng.pbobjc.h"
+#import "Remote/FSTWatchChange.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTSerializerBeta (Test)
+- (GCFSValue *)encodedNull;
+- (GCFSValue *)encodedBool:(BOOL)value;
+- (GCFSValue *)encodedDouble:(double)value;
+- (GCFSValue *)encodedInteger:(int64_t)value;
+- (GCFSValue *)encodedString:(NSString *)value;
+- (GCFSValue *)encodedDate:(NSDate *)value;
+
+- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask;
+- (NSMutableArray<GCFSDocumentTransform_FieldTransform *> *)encodedFieldTransforms:
+ (NSArray<FSTFieldTransform *> *)fieldTransforms;
+
+- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter;
+@end
+
+@interface GCFSStructuredQuery_Order (Test)
++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending;
+@end
+
+@implementation GCFSStructuredQuery_Order (Test)
+
++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending {
+ GCFSStructuredQuery_Order *order = [GCFSStructuredQuery_Order message];
+ order.field.fieldPath = property;
+ order.direction = ascending ? GCFSStructuredQuery_Direction_Ascending
+ : GCFSStructuredQuery_Direction_Descending;
+ return order;
+}
+@end
+
+@interface FSTSerializerBetaTests : XCTestCase
+@property(nonatomic, strong) FSTSerializerBeta *serializer;
+@end
+
+@implementation FSTSerializerBetaTests
+
+- (void)setUp {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ self.serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID];
+}
+
+- (void)testEncodesNull {
+ FSTFieldValue *model = [FSTNullValue nullValue];
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.nullValue = GPBNullValue_NullValue;
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_NullValue];
+}
+
+- (void)testEncodesBool {
+ NSArray<NSNumber *> *examples = @[ @YES, @NO ];
+ for (NSNumber *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.booleanValue = [example boolValue];
+
+ [self assertRoundTripForModel:model
+ proto:proto
+ type:GCFSValue_ValueType_OneOfCase_BooleanValue];
+ }
+}
+
+- (void)testEncodesIntegers {
+ NSArray<NSNumber *> *examples = @[ @(LLONG_MIN), @(-100), @(-1), @0, @1, @100, @(LLONG_MAX) ];
+ for (NSNumber *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.integerValue = [example longLongValue];
+
+ [self assertRoundTripForModel:model
+ proto:proto
+ type:GCFSValue_ValueType_OneOfCase_IntegerValue];
+ }
+}
+
+- (void)testEncodesDoubles {
+ NSArray<NSNumber *> *examples = @[
+ // normal negative numbers.
+ @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * 1.0 - 1.0), @(-2.0), @(-1.1), @(-1.0), @(-DBL_MIN),
+
+ // negative smallest subnormal, zeroes, positive smallest subnormal
+ @(-0x1.0p-1074), @(-0.0), @(0.0), @(0x1.0p-1074),
+
+ // and the rest
+ @(DBL_MIN), @0.1, @1.1, @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY),
+
+ // NaN.
+ @(0.0 / 0.0)
+ ];
+ for (NSNumber *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.doubleValue = [example doubleValue];
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_DoubleValue];
+ }
+}
+
+- (void)testEncodesStrings {
+ NSArray<NSString *> *examples = @[
+ @"",
+ @"a",
+ @"abc def",
+ @"æ",
+ @"\0\ud7ff\ue000\uffff",
+ @"(╯°□°)╯︵ ┻━┻",
+ ];
+ for (NSString *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.stringValue = example;
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_StringValue];
+ }
+}
+
+- (void)testEncodesDates {
+ NSDateComponents *dateWithNanos = FSTTestDateComponents(2016, 1, 2, 10, 20, 50);
+ dateWithNanos.nanosecond = 500000000;
+
+ NSArray<NSDate *> *examples = @[
+ [[NSCalendar currentCalendar] dateFromComponents:dateWithNanos],
+ FSTTestDate(2016, 6, 17, 10, 50, 15)
+ ];
+
+ GCFSValue *timestamp1 = [GCFSValue message];
+ timestamp1.timestampValue.seconds = 1451730050;
+ timestamp1.timestampValue.nanos = 500000000;
+
+ GCFSValue *timestamp2 = [GCFSValue message];
+ timestamp2.timestampValue.seconds = 1466160615;
+ timestamp2.timestampValue.nanos = 0;
+ NSArray<GCFSValue *> *expectedTimestamps = @[ timestamp1, timestamp2 ];
+
+ for (NSUInteger i = 0; i < [examples count]; i++) {
+ [self assertRoundTripForModel:FSTTestFieldValue(examples[i])
+ proto:expectedTimestamps[i]
+ type:GCFSValue_ValueType_OneOfCase_TimestampValue];
+ }
+}
+
+- (void)testEncodesGeoPoints {
+ NSArray<FIRGeoPoint *> *examples =
+ @[ FSTTestGeoPoint(0, 0), FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-90, 180) ];
+ for (FIRGeoPoint *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.geoPointValue = [GTPLatLng message];
+ proto.geoPointValue.latitude = example.latitude;
+ proto.geoPointValue.longitude = example.longitude;
+
+ [self assertRoundTripForModel:model
+ proto:proto
+ type:GCFSValue_ValueType_OneOfCase_GeoPointValue];
+ }
+}
+
+- (void)testEncodesBlobs {
+ NSArray<NSData *> *examples = @[
+ FSTTestData(-1),
+ FSTTestData(0, -1),
+ FSTTestData(0, 1, 2, -1),
+ FSTTestData(255, -1),
+ FSTTestData(0, 1, 255, -1),
+ ];
+ for (NSData *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.bytesValue = example;
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_BytesValue];
+ }
+}
+
+- (void)testEncodesResourceNames {
+ FSTDocumentKeyReference *reference = FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar");
+ GCFSValue *proto = [GCFSValue message];
+ proto.referenceValue = @"projects/project/databases/(default)/documents/foo/bar";
+
+ [self assertRoundTripForModel:FSTTestFieldValue(reference)
+ proto:proto
+ type:GCFSValue_ValueType_OneOfCase_ReferenceValue];
+}
+
+- (void)testEncodesArrays {
+ FSTFieldValue *model = FSTTestFieldValue(@[ @YES, @"foo" ]);
+
+ GCFSValue *proto = [GCFSValue message];
+ [proto.arrayValue.valuesArray addObjectsFromArray:@[
+ [self.serializer encodedBool:YES], [self.serializer encodedString:@"foo"]
+ ]];
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_ArrayValue];
+}
+
+- (void)testEncodesEmptyMap {
+ FSTFieldValue *model = [FSTObjectValue objectValue];
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.mapValue = [GCFSMapValue message];
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue];
+}
+
+- (void)testEncodesNestedObjects {
+ FSTFieldValue *model = FSTTestFieldValue(@{
+ @"b" : @YES,
+ @"d" : @(DBL_MAX),
+ @"i" : @1,
+ @"n" : [NSNull null],
+ @"s" : @"foo",
+ @"a" : @[ @2, @"bar", @{@"b" : @NO} ],
+ @"o" : @{
+ @"d" : @100,
+ @"nested" : @{@"e" : @(LLONG_MIN)},
+ },
+ });
+
+ GCFSValue *innerObject = [GCFSValue message];
+ innerObject.mapValue.fields[@"b"] = [self.serializer encodedBool:NO];
+
+ GCFSValue *middleArray = [GCFSValue message];
+ [middleArray.arrayValue.valuesArray addObjectsFromArray:@[
+ [self.serializer encodedInteger:2], [self.serializer encodedString:@"bar"], innerObject
+ ]];
+
+ innerObject = [GCFSValue message];
+ innerObject.mapValue.fields[@"e"] = [self.serializer encodedInteger:LLONG_MIN];
+
+ GCFSValue *middleObject = [GCFSValue message];
+ [middleObject.mapValue.fields addEntriesFromDictionary:@{
+ @"d" : [self.serializer encodedInteger:100],
+ @"nested" : innerObject
+ }];
+
+ GCFSValue *proto = [GCFSValue message];
+ [proto.mapValue.fields addEntriesFromDictionary:@{
+ @"b" : [self.serializer encodedBool:YES],
+ @"d" : [self.serializer encodedDouble:DBL_MAX],
+ @"i" : [self.serializer encodedInteger:1],
+ @"n" : [self.serializer encodedNull],
+ @"s" : [self.serializer encodedString:@"foo"],
+ @"a" : middleArray,
+ @"o" : middleObject
+ }];
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue];
+}
+
+- (void)assertRoundTripForModel:(FSTFieldValue *)model
+ proto:(GCFSValue *)value
+ type:(GCFSValue_ValueType_OneOfCase)type {
+ GCFSValue *actualProto = [self.serializer encodedFieldValue:model];
+ XCTAssertEqual(actualProto.valueTypeOneOfCase, type);
+ XCTAssertEqualObjects(actualProto, value);
+
+ FSTFieldValue *actualModel = [self.serializer decodedFieldValue:value];
+ XCTAssertEqualObjects(actualModel, model);
+}
+
+- (void)testEncodesSetMutation {
+ FSTSetMutation *mutation = FSTTestSetMutation(@"docs/1", @{ @"a" : @"b", @"num" : @1 });
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key];
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)testEncodesPatchMutation {
+ FSTPatchMutation *mutation =
+ FSTTestPatchMutation(@"docs/1",
+ @{ @"a" : @"b",
+ @"num" : @1,
+ @"some.de\\\\ep.th\\ing'" : @2 },
+ nil);
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key];
+ proto.updateMask = [self.serializer encodedFieldMask:mutation.fieldMask];
+ proto.currentDocument.exists = YES;
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)testEncodesDeleteMutation {
+ FSTDeleteMutation *mutation = FSTTestDeleteMutation(@"docs/1");
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.delete_p = @"projects/p/databases/d/documents/docs/1";
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)testEncodesTransformMutation {
+ FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @[ @"a", @"bar.baz" ]);
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.transform = [GCFSDocumentTransform message];
+ proto.transform.document = [self.serializer encodedDocumentKey:mutation.key];
+ proto.transform.fieldTransformsArray =
+ [self.serializer encodedFieldTransforms:mutation.fieldTransforms];
+ proto.currentDocument.exists = YES;
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)testEncodesSetMutationWithPrecondition {
+ FSTSetMutation *mutation = [[FSTSetMutation alloc]
+ initWithKey:FSTTestDocKey(@"foo/bar")
+ value:FSTTestObjectValue(
+ @{ @"a" : @"b",
+ @"num" : @1 })
+ precondition:[FSTPrecondition preconditionWithUpdateTime:FSTTestVersion(4)]];
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key];
+ proto.currentDocument.updateTime =
+ [self.serializer encodedTimestamp:[[FSTTimestamp alloc] initWithSeconds:0 nanos:4000]];
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)assertRoundTripForMutation:(FSTMutation *)mutation proto:(GCFSWrite *)proto {
+ GCFSWrite *actualProto = [self.serializer encodedMutation:mutation];
+ XCTAssertEqualObjects(actualProto, proto);
+
+ FSTMutation *actualMutation = [self.serializer decodedMutation:proto];
+ XCTAssertEqualObjects(actualMutation, mutation);
+}
+
+- (void)testRoundTripSpecialFieldNames {
+ FSTMutation *set = FSTTestSetMutation(@"collection/key", @{
+ @"field" : [NSString stringWithFormat:@"field %d", 1],
+ @"field.dot" : @2,
+ @"field\\slash" : @3
+ });
+ GCFSWrite *encoded = [self.serializer encodedMutation:set];
+ FSTMutation *decoded = [self.serializer decodedMutation:encoded];
+ XCTAssertEqualObjects(set, decoded);
+}
+
+- (void)testEncodesListenRequestLabels {
+ FSTQuery *query = FSTTestQuery(@"collection/key");
+ FSTQueryData *queryData =
+ [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeListen];
+
+ NSDictionary<NSString *, NSString *> *result =
+ [self.serializer encodedListenRequestLabelsForQueryData:queryData];
+ XCTAssertNil(result);
+
+ queryData =
+ [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeLimboResolution];
+ result = [self.serializer encodedListenRequestLabelsForQueryData:queryData];
+ XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"});
+
+ queryData = [[FSTQueryData alloc] initWithQuery:query
+ targetID:2
+ purpose:FSTQueryPurposeExistenceFilterMismatch];
+ result = [self.serializer encodedListenRequestLabelsForQueryData:queryData];
+ XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"});
+}
+
+- (void)testEncodesRelationFilter {
+ FSTRelationFilter *input = FSTTestFilter(@"item.part.top", @"==", @"food");
+ GCFSStructuredQuery_Filter *actual = [self.serializer encodedRelationFilter:input];
+
+ GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message];
+ GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter;
+ prop.field.fieldPath = @"item.part.top";
+ prop.op = GCFSStructuredQuery_FieldFilter_Operator_Equal;
+ prop.value.stringValue = @"food";
+ XCTAssertEqualObjects(actual, expected);
+}
+
+#pragma mark - encodedQuery
+
+- (void)testEncodesFirstLevelKeyQueries {
+ FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs/1")];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ [expected.documents.documentsArray addObject:@"projects/p/databases/d/documents/docs/1"];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesFirstLevelAncestorQueries {
+ FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"messages")];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"messages";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesNestedAncestorQueries {
+ FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"attachments";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesSingleFiltersAtFirstLevelCollections {
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")]
+ queryByAddingFilter:FSTTestFilter(@"prop", @"<", @(42))];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+
+ GCFSStructuredQuery_FieldFilter *filter = expected.query.structuredQuery.where.fieldFilter;
+ filter.field.fieldPath = @"prop";
+ filter.op = GCFSStructuredQuery_FieldFilter_Operator_LessThan;
+ filter.value.integerValue = 42;
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesMultipleFiltersOnDeeperCollections {
+ FSTQuery *q = [[[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")]
+ queryByAddingFilter:FSTTestFilter(@"prop", @">=", @(42))]
+ queryByAddingFilter:FSTTestFilter(@"author", @"==", @"dimond")];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"attachments";
+ [expected.query.structuredQuery.fromArray addObject:from];
+
+ GCFSStructuredQuery_Filter *filter1 = [GCFSStructuredQuery_Filter message];
+ GCFSStructuredQuery_FieldFilter *field1 = filter1.fieldFilter;
+ field1.field.fieldPath = @"prop";
+ field1.op = GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual;
+ field1.value.integerValue = 42;
+
+ GCFSStructuredQuery_Filter *filter2 = [GCFSStructuredQuery_Filter message];
+ GCFSStructuredQuery_FieldFilter *field2 = filter2.fieldFilter;
+ field2.field.fieldPath = @"author";
+ field2.op = GCFSStructuredQuery_FieldFilter_Operator_Equal;
+ field2.value.stringValue = @"dimond";
+
+ GCFSStructuredQuery_CompositeFilter *composite =
+ expected.query.structuredQuery.where.compositeFilter;
+ composite.op = GCFSStructuredQuery_CompositeFilter_Operator_And;
+ [composite.filtersArray addObject:filter1];
+ [composite.filtersArray addObject:filter2];
+
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesNullFilter {
+ [self unaryFilterTestWithValue:[NSNull null]
+ expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNull];
+}
+
+- (void)testEncodesNanFilter {
+ [self unaryFilterTestWithValue:@(NAN)
+ expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNan];
+}
+
+- (void)unaryFilterTestWithValue:(id)value
+ expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator)
+ operator{
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")]
+ queryByAddingFilter:FSTTestFilter(@"prop", @"==", value)];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+
+ GCFSStructuredQuery_UnaryFilter *filter = expected.query.structuredQuery.where.unaryFilter;
+ filter.field.fieldPath = @"prop";
+ filter.op = operator;
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesSortOrders {
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")]
+ queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop")
+ ascending:YES]];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesSortOrdersDescending {
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")]
+ queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop")
+ ascending:NO]];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"attachments";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:NO]];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:NO]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesLimits {
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] queryBySettingLimit:26];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.query.structuredQuery.limit.value = 26;
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesResumeTokens {
+ FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs")];
+ FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q
+ targetID:1
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:[FSTSnapshotVersion noVersion]
+ resumeToken:FSTTestData(1, 2, 3, -1)];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+ expected.resumeToken = FSTTestData(1, 2, 3, -1);
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (FSTQueryData *)queryDataForQuery:(FSTQuery *)query {
+ return [[FSTQueryData alloc] initWithQuery:query
+ targetID:1
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:[FSTSnapshotVersion noVersion]
+ resumeToken:[NSData data]];
+}
+
+- (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget *)proto {
+ // Verify that the encoded FSTQueryData matches the target.
+ GCFSTarget *actualProto = [self.serializer encodedTarget:queryData];
+ XCTAssertEqualObjects(actualProto, proto);
+
+ // We don't have deserialization logic for full targets since they're not used for RPC
+ // interaction, but the query deserialization only *is* used for the local store.
+ FSTQuery *actualModel;
+ if (proto.targetTypeOneOfCase == GCFSTarget_TargetType_OneOfCase_Query) {
+ actualModel = [self.serializer decodedQueryFromQueryTarget:proto.query];
+ } else {
+ actualModel = [self.serializer decodedQueryFromDocumentsTarget:proto.documents];
+ }
+ XCTAssertEqualObjects(actualModel, queryData.query);
+}
+
+- (void)testConvertsTargetChangeWithAdded {
+ FSTWatchChange *expected =
+ [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateAdded
+ targetIDs:@[ @1, @4 ]
+ resumeToken:[NSData data]
+ cause:nil];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Add;
+ [listenResponse.targetChange.targetIdsArray addValue:1];
+ [listenResponse.targetChange.targetIdsArray addValue:4];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsTargetChangeWithRemoved {
+ FSTWatchChange *expected = [[FSTWatchTargetChange alloc]
+ initWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:@[ @1, @4 ]
+ resumeToken:FSTTestData(0, 1, 2, -1)
+ cause:[NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodePermissionDenied
+ userInfo:@{
+ NSLocalizedDescriptionKey : @"Error message",
+ }]];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Remove;
+ listenResponse.targetChange.cause.code = FIRFirestoreErrorCodePermissionDenied;
+ listenResponse.targetChange.cause.message = @"Error message";
+ listenResponse.targetChange.resumeToken = FSTTestData(0, 1, 2, -1);
+ [listenResponse.targetChange.targetIdsArray addValue:1];
+ [listenResponse.targetChange.targetIdsArray addValue:4];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsTargetChangeWithNoChange {
+ FSTWatchChange *expected =
+ [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @1, @4 ]
+ resumeToken:[NSData data]
+ cause:nil];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_NoChange;
+ [listenResponse.targetChange.targetIdsArray addValue:1];
+ [listenResponse.targetChange.targetIdsArray addValue:4];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsDocumentChangeWithTargetIds {
+ FSTWatchChange *expected = [[FSTDocumentWatchChange alloc]
+ initWithUpdatedTargetIDs:@[ @1, @2 ]
+ removedTargetIDs:@[]
+ documentKey:FSTTestDocKey(@"coll/1")
+ document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1";
+ listenResponse.documentChange.document.updateTime.nanos = 5000;
+ GCFSValue *fooValue = [GCFSValue message];
+ fooValue.stringValue = @"bar";
+ [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"];
+ [listenResponse.documentChange.targetIdsArray addValue:1];
+ [listenResponse.documentChange.targetIdsArray addValue:2];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsDocumentChangeWithRemovedTargetIds {
+ FSTWatchChange *expected = [[FSTDocumentWatchChange alloc]
+ initWithUpdatedTargetIDs:@[ @2 ]
+ removedTargetIDs:@[ @1 ]
+ documentKey:FSTTestDocKey(@"coll/1")
+ document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1";
+ listenResponse.documentChange.document.updateTime.nanos = 5000;
+ GCFSValue *fooValue = [GCFSValue message];
+ fooValue.stringValue = @"bar";
+ [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"];
+ [listenResponse.documentChange.removedTargetIdsArray addValue:1];
+ [listenResponse.documentChange.targetIdsArray addValue:2];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsDocumentChangeWithDeletions {
+ FSTWatchChange *expected =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1, @2 ]
+ documentKey:FSTTestDocKey(@"coll/1")
+ document:FSTTestDeletedDoc(@"coll/1", 5)];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.documentDelete.document = @"projects/p/databases/d/documents/coll/1";
+ listenResponse.documentDelete.readTime.nanos = 5000;
+ [listenResponse.documentDelete.removedTargetIdsArray addValue:1];
+ [listenResponse.documentDelete.removedTargetIdsArray addValue:2];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsDocumentChangeWithRemoves {
+ FSTWatchChange *expected =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1, @2 ]
+ documentKey:FSTTestDocKey(@"coll/1")
+ document:nil];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.documentRemove.document = @"projects/p/databases/d/documents/coll/1";
+ [listenResponse.documentRemove.removedTargetIdsArray addValue:1];
+ [listenResponse.documentRemove.removedTargetIdsArray addValue:2];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTStreamTests.m b/Firestore/Example/Tests/Remote/FSTStreamTests.m
new file mode 100644
index 0000000..f27b200
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTStreamTests.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 "Remote/FSTDatastore.h"
+
+#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import "Auth/FSTEmptyCredentialsProvider.h"
+#import "Core/FSTDatabaseInfo.h"
+#import "Model/FSTDatabaseID.h"
+#import "Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h"
+#import "Util/FSTDispatchQueue.h"
+
+/** Expose otherwise private methods for testing. */
+@interface FSTStream (Testing)
+
+- (void)writesFinishedWithError:(NSError *_Nullable)error;
+
+@end
+
+@interface FSTStreamTests : XCTestCase
+@end
+
+@implementation FSTStreamTests {
+ dispatch_queue_t _testQueue;
+ FSTDatabaseInfo *_databaseInfo;
+ FSTDispatchQueue *_workerDispatchQueue;
+ id<FSTCredentialsProvider> _credentials;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID];
+ _databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"test"
+ host:@"test-host"
+ sslEnabled:NO];
+
+ _testQueue = dispatch_queue_create("com.firebase.testing", DISPATCH_QUEUE_SERIAL);
+ _workerDispatchQueue = [FSTDispatchQueue queueWith:_testQueue];
+ _credentials = [[FSTEmptyCredentialsProvider alloc] init];
+}
+
+- (void)tearDown {
+ [super tearDown];
+}
+
+- (void)testWatchStreamStop {
+ id delegate = OCMStrictProtocolMock(@protocol(FSTWatchStreamDelegate));
+
+ FSTWatchStream *stream =
+ OCMPartialMock([[FSTWatchStream alloc] initWithDatabase:_databaseInfo
+ workerDispatchQueue:_workerDispatchQueue
+ credentials:_credentials
+ responseMessageClass:[GCFSWriteResponse class]
+ delegate:delegate]);
+ OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil);
+
+ // Start the stream up but that's not really the interesting bit. This is complicated by the fact
+ // that startup involves redispatching after credentials are returned.
+ dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0);
+ OCMStub([delegate watchStreamDidOpen]).andDo(^(NSInvocation *invocation) {
+ dispatch_semaphore_signal(openCompleted);
+ });
+ dispatch_async(_testQueue, ^{
+ [stream start];
+ });
+ dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER);
+ OCMVerifyAll(delegate);
+
+ // Stop must not call watchStreamDidClose because the full implementation of the delegate could
+ // attempt to restart the stream in the event it had pending watches.
+ dispatch_sync(_testQueue, ^{
+ [stream stop];
+ });
+ OCMVerifyAll(delegate);
+
+ // Simulate a final callback from GRPC
+ [stream writesFinishedWithError:nil];
+ // Drain queue
+ dispatch_sync(_testQueue, ^{
+ });
+ OCMVerifyAll(delegate);
+}
+
+- (void)testWriteStreamStop {
+ id delegate = OCMStrictProtocolMock(@protocol(FSTWriteStreamDelegate));
+
+ FSTWriteStream *stream =
+ OCMPartialMock([[FSTWriteStream alloc] initWithDatabase:_databaseInfo
+ workerDispatchQueue:_workerDispatchQueue
+ credentials:_credentials
+ responseMessageClass:[GCFSWriteResponse class]
+ delegate:delegate]);
+ OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil);
+
+ // Start the stream up but that's not really the interesting bit.
+ dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0);
+ OCMStub([delegate writeStreamDidOpen]).andDo(^(NSInvocation *invocation) {
+ dispatch_semaphore_signal(openCompleted);
+ });
+ dispatch_async(_testQueue, ^{
+ [stream start];
+ });
+ dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER);
+ OCMVerifyAll(delegate);
+
+ // Stop must not call writeStreamDidClose because the full implementation of this delegate could
+ // attempt to restart the stream in the event it had pending writes.
+ dispatch_sync(_testQueue, ^{
+ [stream stop];
+ });
+ OCMVerifyAll(delegate);
+
+ // Simulate a final callback from GRPC
+ [stream writesFinishedWithError:nil];
+ // Drain queue
+ dispatch_sync(_testQueue, ^{
+ });
+ OCMVerifyAll(delegate);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h
new file mode 100644
index 0000000..f94fe05
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.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 "Core/FSTTypes.h"
+#import "Remote/FSTWatchChange.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** FSTWatchTargetChange is a change to a watch target. */
+@interface FSTWatchTargetChange (Testing)
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs;
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ cause:(nullable NSError *)cause;
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ resumeToken:(nullable NSData *)resumeToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m
new file mode 100644
index 0000000..cb5e479
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.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 "FSTWatchChange+Testing.h"
+
+#import "Model/FSTDocument.h"
+#import "Remote/FSTWatchChange.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTWatchTargetChange (Testing)
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs {
+ return [[FSTWatchTargetChange alloc] initWithState:state
+ targetIDs:targetIDs
+ resumeToken:[NSData data]
+ cause:nil];
+}
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ cause:(nullable NSError *)cause {
+ return [[FSTWatchTargetChange alloc] initWithState:state
+ targetIDs:targetIDs
+ resumeToken:[NSData data]
+ cause:cause];
+}
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ resumeToken:(nullable NSData *)resumeToken {
+ return [[FSTWatchTargetChange alloc] initWithState:state
+ targetIDs:targetIDs
+ resumeToken:resumeToken
+ cause:nil];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m
new file mode 100644
index 0000000..ccbd644
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m
@@ -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 "Remote/FSTWatchChange.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTDocument.h"
+#import "Remote/FSTExistenceFilter.h"
+
+#import "FSTHelpers.h"
+#import "FSTWatchChange+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTWatchChangeTests : XCTestCase
+@end
+
+@implementation FSTWatchChangeTests
+
+- (void)testDocumentChange {
+ FSTMaybeDocument *doc = FSTTestDoc(@"a/b", 1, @{}, NO);
+ FSTDocumentWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ]
+ removedTargetIDs:@[ @4, @5 ]
+ documentKey:doc.key
+ document:doc];
+ XCTAssertEqual(change.updatedTargetIDs.count, 3);
+ XCTAssertEqual(change.removedTargetIDs.count, 2);
+ // Testing object identity here is fine.
+ XCTAssertEqual(change.document, doc);
+}
+
+- (void)testExistenceFilterChange {
+ FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:7];
+ FSTExistenceFilterWatchChange *change =
+ [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:5];
+ XCTAssertEqual(change.filter.count, 7);
+ XCTAssertEqual(change.targetID, 5);
+}
+
+- (void)testWatchTargetChange {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @1, @2 ]
+ cause:nil];
+ XCTAssertEqual(change.state, FSTWatchTargetChangeStateReset);
+ XCTAssertEqual(change.targetIDs.count, 2);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m
new file mode 100644
index 0000000..88b3f12
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.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 "FSTSpecTests.h"
+
+#import "Local/FSTLevelDB.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTSyncEngineTestDriver.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An implementation of FSTSpecTests that uses the LevelDB implementation of local storage.
+ *
+ * See the FSTSpecTests class comments for more information about how this works.
+ */
+@interface FSTLevelDBSpecTests : FSTSpecTests
+@end
+
+@implementation FSTLevelDBSpecTests
+
+/** Overrides -[FSTSpecTests persistence] */
+- (id<FSTPersistence>)persistence {
+ return [FSTPersistenceTestHelpers levelDBPersistence];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m
new file mode 100644
index 0000000..9cf1f39
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m
@@ -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 "FSTSpecTests.h"
+
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTSyncEngineTestDriver.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An implementation of FSTSpecTests that uses the memory-only implementation of local storage.
+ *
+ * @see the FSTSpecTests class comments for more information about how this works.
+ */
+@interface FSTMemorySpecTests : FSTSpecTests
+@end
+
+@implementation FSTMemorySpecTests
+
+/** Overrides -[FSTSpecTests persistence] */
+- (id<FSTPersistence>)persistence {
+ return [FSTPersistenceTestHelpers memoryPersistence];
+}
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h
new file mode 100644
index 0000000..4ff1220
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "Remote/FSTDatastore.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMockDatastore : FSTDatastore
+
++ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue;
+
+#pragma mark - Watch Stream manipulation.
+
+/** Injects an Added WatchChange containing the given targetIDs. */
+- (void)writeWatchTargetAddedWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs;
+
+/** Injects an Added WatchChange that marks the given targetIDs current. */
+- (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ resumeToken:(NSData *)resumeToken;
+
+/** Injects a WatchChange as though it had come from the backend. */
+- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap;
+
+/** Injects a stream failure as though it had come from the backend. */
+- (void)failWatchStreamWithError:(NSError *)error;
+
+/** Returns the set of active targets on the watch stream. */
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)activeTargets;
+
+/** Helper method to expose watch stream state to verify in tests. */
+- (BOOL)isWatchStreamOpen;
+
+#pragma mark - Write Stream manipulation.
+
+/**
+ * Returns the next write that was "sent to the backend", failing if there are no queued sent
+ */
+- (NSArray<FSTMutation *> *)nextSentWrite;
+
+/** Returns the number of writes that have been sent to the backend but not waited on yet. */
+- (int)writesSent;
+
+/** Injects a write ack as though it had come from the backend in response to a write. */
+- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)results;
+
+/** Injects a stream failure as though it had come from the backend. */
+- (void)failWriteWithError:(NSError *_Nullable)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m
new file mode 100644
index 0000000..1a1f659
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTMockDatastore.h"
+
+#import "Auth/FSTEmptyCredentialsProvider.h"
+#import "Core/FSTDatabaseInfo.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTMutation.h"
+#import "Util/FSTAssert.h"
+#import "Util/FSTLogger.h"
+
+#import "FSTWatchChange+Testing.h"
+
+@class GRPCProtoCall;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTMockWatchStream
+
+@interface FSTMockWatchStream : FSTWatchStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ delegate:(id<FSTWatchStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWatchStreamDelegate>)delegate NS_UNAVAILABLE;
+
+@property(nonatomic, assign) BOOL open;
+
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *activeTargets;
+
+@end
+
+@implementation FSTMockWatchStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ delegate:(id<FSTWatchStreamDelegate>)delegate {
+ self = [super initWithDatabase:database
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials
+ responseMessageClass:[FSTWatchChange class]
+ delegate:delegate];
+ if (self) {
+ FSTAssert(database, @"Database must not be nil");
+ _activeTargets = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+#pragma mark - Overridden FSTWatchStream methods.
+
+- (void)start {
+ FSTAssert(!self.open, @"Trying to start already started watch stream");
+ self.open = YES;
+ [self handleStreamOpen];
+}
+
+- (BOOL)isOpen {
+ return self.open;
+}
+
+- (BOOL)isStarted {
+ return self.open;
+}
+
+- (void)handleStreamOpen {
+ [self.delegate watchStreamDidOpen];
+}
+
+- (void)watchQuery:(FSTQueryData *)query {
+ FSTLog(@"watchQuery: %d: %@", query.targetID, query.query);
+ // Snapshot version is ignored on the wire
+ FSTQueryData *sentQueryData =
+ [query queryDataByReplacingSnapshotVersion:[FSTSnapshotVersion noVersion]
+ resumeToken:query.resumeToken];
+ self.activeTargets[@(query.targetID)] = sentQueryData;
+}
+
+- (void)unwatchTargetID:(FSTTargetID)targetID {
+ FSTLog(@"unwatchTargetID: %d", targetID);
+ [self.activeTargets removeObjectForKey:@(targetID)];
+}
+
+- (void)failStreamWithError:(NSError *)error {
+ self.open = NO;
+ [self.delegate watchStreamDidClose:error];
+}
+
+#pragma mark - Helper methods.
+
+- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap {
+ if ([change isKindOfClass:[FSTWatchTargetChange class]]) {
+ FSTWatchTargetChange *targetChange = (FSTWatchTargetChange *)change;
+ if (targetChange.cause) {
+ for (NSNumber *targetID in targetChange.targetIDs) {
+ if (!self.activeTargets[targetID]) {
+ // Technically removing an unknown target is valid (e.g. it could race with a
+ // server-side removal), but we want to pay extra careful attention in tests
+ // that we only remove targets we listened too.
+ FSTFail(@"Removing a non-active target");
+ }
+ [self.activeTargets removeObjectForKey:targetID];
+ }
+ }
+ }
+ [self.delegate watchStreamDidChange:change snapshotVersion:snap];
+}
+
+@end
+
+#pragma mark - FSTMockWriteStream
+
+@interface FSTMockWriteStream : FSTWriteStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ delegate:(id<FSTWriteStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWatchStreamDelegate>)delegate NS_UNAVAILABLE;
+
+@property(nonatomic, assign) BOOL open;
+@property(nonatomic, strong, readonly) NSMutableArray<NSArray<FSTMutation *> *> *sentMutations;
+
+@end
+
+@implementation FSTMockWriteStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ delegate:(id<FSTWriteStreamDelegate>)delegate {
+ self = [super initWithDatabase:database
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials
+ responseMessageClass:[FSTMutationResult class]
+ delegate:delegate];
+ if (self) {
+ _sentMutations = [NSMutableArray array];
+ }
+ return self;
+}
+
+#pragma mark - Overridden FSTWriteStream methods.
+
+- (void)start {
+ FSTAssert(!self.open, @"Trying to start already started write stream");
+ self.open = YES;
+ [self.sentMutations removeAllObjects];
+ [self handleStreamOpen];
+}
+
+- (BOOL)isOpen {
+ return self.open;
+}
+
+- (BOOL)isStarted {
+ return self.open;
+}
+
+- (void)writeHandshake {
+ self.handshakeComplete = YES;
+ [self.delegate writeStreamDidCompleteHandshake];
+}
+
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
+ [self.sentMutations addObject:mutations];
+}
+
+- (void)handleStreamOpen {
+ [self.delegate writeStreamDidOpen];
+}
+
+#pragma mark - Helper methods.
+
+/** Injects a write ack as though it had come from the backend in response to a write. */
+- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)results {
+ [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results];
+}
+
+/** Injects a failed write response as though it had come from the backend. */
+- (void)failStreamWithError:(NSError *)error {
+ self.open = NO;
+ [self.delegate writeStreamDidClose:error];
+}
+
+/**
+ * Returns the next write that was "sent to the backend", failing if there are no queued sent
+ */
+- (NSArray<FSTMutation *> *)nextSentWrite {
+ FSTAssert(self.sentMutations.count > 0,
+ @"Writes need to happen before you can call nextSentWrite.");
+ NSArray<FSTMutation *> *result = [self.sentMutations objectAtIndex:0];
+ [self.sentMutations removeObjectAtIndex:0];
+ return result;
+}
+
+/**
+ * Returns the number of mutations that have been sent to the backend but not retrieved via
+ * nextSentWrite yet.
+ */
+- (int)sentMutationsCount {
+ return (int)self.sentMutations.count;
+}
+
+@end
+
+#pragma mark - FSTMockDatastore
+
+@interface FSTMockDatastore ()
+@property(nonatomic, strong, nullable) FSTMockWatchStream *watchStream;
+@property(nonatomic, strong, nullable) FSTMockWriteStream *writeStream;
+
+/** Properties implemented in FSTDatastore that are nonpublic. */
+@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue;
+@property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentials;
+
+@end
+
+@implementation FSTMockDatastore
+
++ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"project" database:@"database"];
+ FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"persistence"
+ host:@"host"
+ sslEnabled:NO];
+
+ FSTEmptyCredentialsProvider *credentials = [[FSTEmptyCredentialsProvider alloc] init];
+
+ return [[FSTMockDatastore alloc] initWithDatabaseInfo:databaseInfo
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials];
+}
+
+#pragma mark - Overridden FSTDatastore methods.
+
+- (FSTWatchStream *)createWatchStreamWithDelegate:(id<FSTWatchStreamDelegate>)delegate {
+ FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil");
+ self.watchStream = [[FSTMockWatchStream alloc] initWithDatabase:self.databaseInfo
+ workerDispatchQueue:self.workerDispatchQueue
+ credentials:self.credentials
+ delegate:delegate];
+ return self.watchStream;
+}
+
+- (FSTWriteStream *)createWriteStreamWithDelegate:(id<FSTWriteStreamDelegate>)delegate {
+ FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil");
+ self.writeStream = [[FSTMockWriteStream alloc] initWithDatabase:self.databaseInfo
+ workerDispatchQueue:self.workerDispatchQueue
+ credentials:self.credentials
+ delegate:delegate];
+ return self.writeStream;
+}
+
+- (void)authorizeAndStartRPC:(GRPCProtoCall *)rpc completion:(FSTVoidErrorBlock)completion {
+ FSTFail(@"FSTMockDatastore shouldn't be starting any RPCs.");
+}
+
+#pragma mark - Method exposed for tests to call.
+
+- (NSArray<FSTMutation *> *)nextSentWrite {
+ return [self.writeStream nextSentWrite];
+}
+
+- (int)writesSent {
+ return [self.writeStream sentMutationsCount];
+}
+
+- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)results {
+ [self.writeStream ackWriteWithVersion:commitVersion mutationResults:results];
+}
+
+- (void)failWriteWithError:(NSError *_Nullable)error {
+ [self.writeStream failStreamWithError:error];
+}
+
+- (void)writeWatchTargetAddedWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded
+ targetIDs:targetIDs
+ cause:nil];
+ [self writeWatchChange:change snapshotVersion:[FSTSnapshotVersion noVersion]];
+}
+
+- (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ resumeToken:(NSData *)resumeToken {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:targetIDs
+ resumeToken:resumeToken];
+ [self writeWatchChange:change snapshotVersion:snapshotVersion];
+}
+
+- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap {
+ [self.watchStream writeWatchChange:change snapshotVersion:snap];
+}
+
+- (void)failWatchStreamWithError:(NSError *)error {
+ [self.watchStream failStreamWithError:error];
+}
+
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)activeTargets {
+ return [self.watchStream.activeTargets copy];
+}
+
+- (BOOL)isWatchStreamOpen {
+ return self.watchStream.isOpen;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.h b/Firestore/Example/Tests/SpecTests/FSTSpecTests.h
new file mode 100644
index 0000000..3a3dbb2
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.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 <XCTest/XCTest.h>
+
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTSpecTests run a set of portable event specifications from JSON spec files against a
+ * special isolated version of the Firestore client that allows precise control over when events
+ * are delivered. This allows us to test client behavior in a very reliable, deterministic way,
+ * including edge cases that would be difficult to reliably reproduce in a full integration test.
+ *
+ * Both events from user code (adding/removing listens, performing mutations) and events from the
+ * Datastore are simulated, while installing as much of the system in between as possible.
+ *
+ * FSTSpecTests is an abstract base class that must be subclassed to test against a specific local
+ * store implementation. To create a new variant of FSTSpecTests:
+ *
+ * + Subclass FSTSpecTests
+ * + override -persistence to create and return an appropriate id<FSTPersistence> implementation.
+ */
+@interface FSTSpecTests : XCTestCase
+
+/** Creates and returns an appropriate id<FSTPersistence> implementation. */
+- (id<FSTPersistence>)persistence;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m
new file mode 100644
index 0000000..f681347
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m
@@ -0,0 +1,642 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTSpecTests.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+#import "Auth/FSTUser.h"
+#import "Core/FSTEventManager.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTViewSnapshot.h"
+#import "Firestore/FIRFirestoreErrors.h"
+#import "Local/FSTEagerGarbageCollector.h"
+#import "Local/FSTNoOpGarbageCollector.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTExistenceFilter.h"
+#import "Remote/FSTWatchChange.h"
+#import "Util/FSTAssert.h"
+#import "Util/FSTClasses.h"
+#import "Util/FSTLogger.h"
+
+#import "FSTHelpers.h"
+#import "FSTSyncEngineTestDriver.h"
+#import "FSTWatchChange+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Disables all other tests; useful for debugging. Multiple tests can have this tag and they'll all
+// be run (but all others won't).
+static NSString *const kExclusiveTag = @"exclusive";
+
+// A tag for tests that should be excluded from execution (on iOS), useful to allow the platforms
+// to temporarily diverge.
+static NSString *const kNoIOSTag = @"no-ios";
+
+@interface FSTSpecTests ()
+@property(nonatomic, strong) FSTSyncEngineTestDriver *driver;
+
+// Some config info for the currently running spec; used when restarting the driver (for doRestart).
+@property(nonatomic, assign) BOOL GCEnabled;
+@property(nonatomic, strong) id<FSTPersistence> driverPersistence;
+@end
+
+@implementation FSTSpecTests
+
+- (id<FSTPersistence>)persistence {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)setUpForSpecWithConfig:(NSDictionary *)config {
+ // Store persistence / GCEnabled so we can re-use it in doRestart.
+ self.driverPersistence = [self persistence];
+ NSNumber *GCEnabled = config[@"useGarbageCollection"];
+ self.GCEnabled = [GCEnabled boolValue];
+ self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence
+ garbageCollector:self.garbageCollector];
+ [self.driver start];
+}
+
+- (void)tearDownForSpec {
+ [self.driver shutdown];
+ [self.driverPersistence shutdown];
+}
+
+/**
+ * Creates the appropriate garbage collector for the test configuration: an eager collector if
+ * GC is enabled or a no-op collector otherwise.
+ */
+- (id<FSTGarbageCollector>)garbageCollector {
+ return self.GCEnabled ? [[FSTEagerGarbageCollector alloc] init]
+ : [[FSTNoOpGarbageCollector alloc] init];
+}
+
+/**
+ * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for
+ * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses.
+ */
+- (BOOL)isTestBaseClass {
+ return [self class] == [FSTSpecTests class];
+}
+
+#pragma mark - Methods for constructing objects from specs.
+
+- (nullable FSTQuery *)parseQuery:(id)querySpec {
+ if ([querySpec isKindOfClass:[NSString class]]) {
+ return [FSTQuery queryWithPath:[FSTResourcePath pathWithString:querySpec]];
+ } else if ([querySpec isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *queryDict = (NSDictionary *)querySpec;
+ NSString *path = queryDict[@"path"];
+ __block FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithString:path]];
+ if (queryDict[@"limit"]) {
+ NSNumber *limit = queryDict[@"limit"];
+ query = [query queryBySettingLimit:limit.integerValue];
+ }
+ if (queryDict[@"filters"]) {
+ NSArray *filters = queryDict[@"filters"];
+ [filters enumerateObjectsUsingBlock:^(NSArray *_Nonnull filter, NSUInteger idx,
+ BOOL *_Nonnull stop) {
+ query = [query queryByAddingFilter:FSTTestFilter(filter[0], filter[1], filter[2])];
+ }];
+ }
+ if (queryDict[@"orderBys"]) {
+ NSArray *orderBys = queryDict[@"orderBys"];
+ [orderBys enumerateObjectsUsingBlock:^(NSArray *_Nonnull orderBy, NSUInteger idx,
+ BOOL *_Nonnull stop) {
+ query = [query queryByAddingSortOrder:FSTTestOrderBy(orderBy[0], orderBy[1])];
+ }];
+ }
+ return query;
+ } else {
+ XCTFail(@"Invalid query: %@", querySpec);
+ return nil;
+ }
+}
+
+- (FSTSnapshotVersion *)parseVersion:(NSNumber *_Nullable)version {
+ return FSTTestVersion(version.longLongValue);
+}
+
+- (FSTDocumentViewChange *)parseChange:(NSArray *)change ofType:(FSTDocumentViewChangeType)type {
+ BOOL hasMutations = NO;
+ for (NSUInteger i = 3; i < change.count; ++i) {
+ if ([change[i] isEqual:@"local"]) {
+ hasMutations = YES;
+ }
+ }
+ NSNumber *version = change[1];
+ FSTDocument *doc = FSTTestDoc(change[0], version.longLongValue, change[2], hasMutations);
+ return [FSTDocumentViewChange changeWithDocument:doc type:type];
+}
+
+#pragma mark - Methods for doing the steps of the spec test.
+
+- (void)doListen:(NSArray *)listenSpec {
+ FSTQuery *query = [self parseQuery:listenSpec[1]];
+ FSTTargetID actualID = [self.driver addUserListenerWithQuery:query];
+
+ FSTTargetID expectedID = [listenSpec[0] intValue];
+ XCTAssertEqual(actualID, expectedID);
+}
+
+- (void)doUnlisten:(NSArray *)unlistenSpec {
+ FSTQuery *query = [self parseQuery:unlistenSpec[1]];
+ [self.driver removeUserListenerWithQuery:query];
+}
+
+- (void)doSet:(NSArray *)setSpec {
+ [self.driver writeUserMutation:FSTTestSetMutation(setSpec[0], setSpec[1])];
+}
+
+- (void)doPatch:(NSArray *)patchSpec {
+ [self.driver writeUserMutation:FSTTestPatchMutation(patchSpec[0], patchSpec[1], nil)];
+}
+
+- (void)doDelete:(NSString *)key {
+ [self.driver writeUserMutation:FSTTestDeleteMutation(key)];
+}
+
+- (void)doWatchAck:(NSArray<NSNumber *> *)ackedTargets snapshot:(NSNumber *)watchSnapshot {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded
+ targetIDs:ackedTargets
+ cause:nil];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+}
+
+- (void)doWatchCurrent:(NSArray<id> *)currentSpec snapshot:(NSNumber *)watchSnapshot {
+ NSArray<NSNumber *> *currentTargets = currentSpec[0];
+ NSData *resumeToken = [currentSpec[1] dataUsingEncoding:NSUTF8StringEncoding];
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:currentTargets
+ resumeToken:resumeToken];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+}
+
+- (void)doWatchRemove:(NSDictionary *)watchRemoveSpec snapshot:(NSNumber *)watchSnapshot {
+ NSError *error = nil;
+ NSDictionary *cause = watchRemoveSpec[@"cause"];
+ if (cause) {
+ int code = ((NSNumber *)cause[@"code"]).intValue;
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey : @"Error from watchRemove.",
+ };
+ error = [NSError errorWithDomain:FIRFirestoreErrorDomain code:code userInfo:userInfo];
+ }
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:watchRemoveSpec[@"targetIds"]
+ cause:error];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+ // Unlike web, the FSTMockDatastore detects a watch removal with cause and will remove active
+ // targets
+}
+
+- (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable)watchSnapshot {
+ if (watchEntity[@"docs"]) {
+ FSTAssert(!watchEntity[@"doc"], @"Exactly one of |doc| or |docs| needs to be set.");
+ int count = 0;
+ NSArray *docs = watchEntity[@"docs"];
+ for (NSDictionary *doc in docs) {
+ count++;
+ bool isLast = (count == docs.count);
+ NSMutableDictionary *watchSpec = [NSMutableDictionary dictionary];
+ watchSpec[@"doc"] = doc;
+ if (watchEntity[@"targets"]) {
+ watchSpec[@"targets"] = watchEntity[@"targets"];
+ }
+ if (watchEntity[@"removedTargets"]) {
+ watchSpec[@"removedTargets"] = watchEntity[@"removedTargets"];
+ }
+ NSNumber *_Nullable version = nil;
+ if (isLast) {
+ version = watchSnapshot;
+ }
+ [self doWatchEntity:watchSpec snapshot:version];
+ }
+ } else if (watchEntity[@"doc"]) {
+ NSArray *docSpec = watchEntity[@"doc"];
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:docSpec[0]];
+ FSTObjectValue *value = FSTTestObjectValue(docSpec[2]);
+ FSTSnapshotVersion *version = [self parseVersion:docSpec[1]];
+ FSTMaybeDocument *doc =
+ [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO];
+ FSTWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"]
+ removedTargetIDs:watchEntity[@"removedTargets"]
+ documentKey:doc.key
+ document:doc];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+ } else if (watchEntity[@"key"]) {
+ FSTDocumentKey *docKey = [FSTDocumentKey keyWithPathString:watchEntity[@"key"]];
+ FSTWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:watchEntity[@"removedTargets"]
+ documentKey:docKey
+ document:nil];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+ } else {
+ FSTFail(@"Either key, doc or docs must be set.");
+ }
+}
+
+- (void)doWatchFilter:(NSArray *)watchFilter snapshot:(NSNumber *_Nullable)watchSnapshot {
+ NSArray<NSNumber *> *targets = watchFilter[0];
+ FSTAssert(targets.count == 1, @"ExistenceFilters currently support exactly one target only.");
+
+ int keyCount = watchFilter.count == 0 ? 0 : (int)watchFilter.count - 1;
+
+ // TODO(dimond): extend this with different existence filters over time.
+ FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:keyCount];
+ FSTExistenceFilterWatchChange *change =
+ [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:targets[0].intValue];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+}
+
+- (void)doWatchReset:(NSArray<NSNumber *> *)watchReset snapshot:(NSNumber *_Nullable)watchSnapshot {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:watchReset
+ cause:nil];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+}
+
+- (void)doWatchStreamClose:(NSDictionary *)closeSpec {
+ NSDictionary *errorSpec = closeSpec[@"error"];
+ int code = ((NSNumber *)(errorSpec[@"code"])).intValue;
+ [self.driver receiveWatchStreamError:code userInfo:errorSpec];
+}
+
+- (void)doWriteAck:(NSDictionary *)spec {
+ FSTSnapshotVersion *version = [self parseVersion:spec[@"version"]];
+ NSNumber *expectUserCallback = spec[@"expectUserCallback"];
+
+ FSTMutationResult *mutationResult =
+ [[FSTMutationResult alloc] initWithVersion:version transformResults:nil];
+ FSTOutstandingWrite *write =
+ [self.driver receiveWriteAckWithVersion:version mutationResults:@[ mutationResult ]];
+
+ if (expectUserCallback.boolValue) {
+ FSTAssert(write.done, @"Write should be done");
+ FSTAssert(!write.error, @"Ack should not fail");
+ }
+}
+
+- (void)doFailWrite:(NSDictionary *)spec {
+ NSDictionary *errorSpec = spec[@"error"];
+ NSNumber *expectUserCallback = spec[@"expectUserCallback"];
+
+ int code = ((NSNumber *)(errorSpec[@"code"])).intValue;
+ FSTOutstandingWrite *write = [self.driver receiveWriteError:code userInfo:errorSpec];
+
+ if (expectUserCallback.boolValue) {
+ FSTAssert(write.done, @"Write should be done");
+ XCTAssertNotNil(write.error, @"Write should have failed");
+ XCTAssertEqualObjects(write.error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(write.error.code, code);
+ }
+}
+
+- (void)doChangeUser:(id)UID {
+ FSTUser *user = [UID isEqual:[NSNull null]] ? [FSTUser unauthenticatedUser]
+ : [[FSTUser alloc] initWithUID:UID];
+ [self.driver changeUser:user];
+}
+
+- (void)doRestart {
+ // Any outstanding user writes should be automatically re-sent, so we want to preserve them
+ // when re-creating the driver.
+ FSTOutstandingWriteQueues *outstandingWrites = self.driver.outstandingWrites;
+
+ [self.driver shutdown];
+
+ // NOTE: We intentionally don't shutdown / re-create driverPersistence, since we want to
+ // preserve the persisted state. This is a bit of a cheat since it means we're not exercising
+ // the initialization / start logic that would normally be hit, but simplifies the plumbing and
+ // allows us to run these tests against FSTMemoryPersistence as well (there would be no way to
+ // re-create FSTMemoryPersistence without losing all persisted state).
+
+ self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence
+ garbageCollector:self.garbageCollector
+ initialUser:self.driver.currentUser
+ outstandingWrites:outstandingWrites];
+ [self.driver start];
+}
+
+- (void)doStep:(NSDictionary *)step {
+ if (step[@"userListen"]) {
+ [self doListen:step[@"userListen"]];
+ } else if (step[@"userUnlisten"]) {
+ [self doUnlisten:step[@"userUnlisten"]];
+ } else if (step[@"userSet"]) {
+ [self doSet:step[@"userSet"]];
+ } else if (step[@"userPatch"]) {
+ [self doPatch:step[@"userPatch"]];
+ } else if (step[@"userDelete"]) {
+ [self doDelete:step[@"userDelete"]];
+ } else if (step[@"watchAck"]) {
+ [self doWatchAck:step[@"watchAck"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchCurrent"]) {
+ [self doWatchCurrent:step[@"watchCurrent"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchRemove"]) {
+ [self doWatchRemove:step[@"watchRemove"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchEntity"]) {
+ [self doWatchEntity:step[@"watchEntity"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchFilter"]) {
+ [self doWatchFilter:step[@"watchFilter"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchReset"]) {
+ [self doWatchReset:step[@"watchReset"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchStreamClose"]) {
+ [self doWatchStreamClose:step[@"watchStreamClose"]];
+ } else if (step[@"watchProto"]) {
+ // watchProto isn't yet used, and it's unclear how to create arbitrary protos from JSON.
+ FSTFail(@"watchProto is not yet supported.");
+ } else if (step[@"writeAck"]) {
+ [self doWriteAck:step[@"writeAck"]];
+ } else if (step[@"failWrite"]) {
+ [self doFailWrite:step[@"failWrite"]];
+ } else if (step[@"changeUser"]) {
+ [self doChangeUser:step[@"changeUser"]];
+ } else if (step[@"restart"]) {
+ [self doRestart];
+ } else {
+ XCTFail(@"Unknown step: %@", step);
+ }
+}
+
+- (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected {
+ FSTQuery *expectedQuery = [self parseQuery:expected[@"query"]];
+ XCTAssertEqualObjects(actual.query, expectedQuery);
+ if ([expected[@"errorCode"] integerValue] != 0) {
+ XCTAssertNotNil(actual.error);
+ XCTAssertEqual(actual.error.code, [expected[@"errorCode"] integerValue]);
+ } else {
+ NSMutableArray *expectedChanges = [NSMutableArray array];
+ NSMutableArray *removed = expected[@"removed"];
+ for (NSArray *changeSpec in removed) {
+ [expectedChanges
+ addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeRemoved]];
+ }
+ NSMutableArray *added = expected[@"added"];
+ for (NSArray *changeSpec in added) {
+ [expectedChanges
+ addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeAdded]];
+ }
+ NSMutableArray *modified = expected[@"modified"];
+ for (NSArray *changeSpec in modified) {
+ [expectedChanges
+ addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeModified]];
+ }
+ NSMutableArray *metadata = expected[@"metadata"];
+ for (NSArray *changeSpec in metadata) {
+ [expectedChanges
+ addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeMetadata]];
+ }
+ XCTAssertEqualObjects(actual.viewSnapshot.documentChanges, expectedChanges);
+
+ BOOL expectedHasPendingWrites =
+ expected[@"hasPendingWrites"] ? [expected[@"hasPendingWrites"] boolValue] : NO;
+ BOOL expectedIsFromCache = expected[@"fromCache"] ? [expected[@"fromCache"] boolValue] : NO;
+ XCTAssertEqual(actual.viewSnapshot.hasPendingWrites, expectedHasPendingWrites,
+ @"hasPendingWrites");
+ XCTAssertEqual(actual.viewSnapshot.isFromCache, expectedIsFromCache, @"isFromCache");
+ }
+}
+
+- (void)validateStepExpectations:(NSMutableArray *_Nullable)stepExpectations {
+ NSArray<FSTQueryEvent *> *events = self.driver.capturedEventsSinceLastCall;
+
+ if (!stepExpectations) {
+ XCTAssertEqual(events.count, 0);
+ for (FSTQueryEvent *event in events) {
+ XCTFail(@"Unexpected event: %@", event);
+ }
+ return;
+ }
+
+ events =
+ [events sortedArrayUsingComparator:^NSComparisonResult(FSTQueryEvent *q1, FSTQueryEvent *q2) {
+ return [q1.query.canonicalID compare:q2.query.canonicalID];
+ }];
+
+ XCTAssertEqual(events.count, stepExpectations.count);
+ NSUInteger i = 0;
+ for (; i < stepExpectations.count && i < events.count; ++i) {
+ [self validateEvent:events[i] matches:stepExpectations[i]];
+ }
+ for (; i < stepExpectations.count; ++i) {
+ XCTFail(@"Missing event: %@", stepExpectations[i]);
+ }
+ for (; i < events.count; ++i) {
+ XCTFail(@"Unexpected event: %@", events[i]);
+ }
+}
+
+- (void)validateStateExpectations:(nullable NSDictionary *)expected {
+ if (expected) {
+ if (expected[@"numOutstandingWrites"]) {
+ XCTAssertEqual([self.driver sentWritesCount], [expected[@"numOutstandingWrites"] intValue]);
+ }
+ if (expected[@"limboDocs"]) {
+ NSMutableSet<FSTDocumentKey *> *expectedLimboDocuments = [NSMutableSet set];
+ NSArray *docNames = expected[@"limboDocs"];
+ for (NSString *name in docNames) {
+ [expectedLimboDocuments addObject:FSTTestDocKey(name)];
+ }
+ // Update the expected limbo documents
+ self.driver.expectedLimboDocuments = expectedLimboDocuments;
+ }
+ if (expected[@"activeTargets"]) {
+ NSMutableDictionary *expectedActiveTargets = [NSMutableDictionary dictionary];
+ [expected[@"activeTargets"] enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString,
+ NSDictionary *queryData,
+ BOOL *stop) {
+ FSTTargetID targetID = [targetIDString intValue];
+ FSTQuery *query = [self parseQuery:queryData[@"query"]];
+ NSData *resumeToken = [queryData[@"resumeToken"] dataUsingEncoding:NSUTF8StringEncoding];
+ // TODO(mcg): populate the purpose of the target once it's possible to encode that in the
+ // spec tests. For now, hard-code that it's a listen despite the fact that it's not always
+ // the right value.
+ expectedActiveTargets[@(targetID)] =
+ [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:[FSTSnapshotVersion noVersion]
+ resumeToken:resumeToken];
+ }];
+ self.driver.expectedActiveTargets = expectedActiveTargets;
+ }
+ }
+
+ // Always validate that the expected limbo docs match the actual limbo docs.
+ [self validateLimboDocuments];
+ // Always validate that the expected active targets match the actual active targets.
+ [self validateActiveTargets];
+}
+
+- (void)validateLimboDocuments {
+ // Make a copy so it can modified while checking against the expected limbo docs.
+ NSMutableDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *actualLimboDocs =
+ [NSMutableDictionary dictionaryWithDictionary:self.driver.currentLimboDocuments];
+
+ // Validate that each limbo doc has an expected active target
+ [actualLimboDocs enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key,
+ FSTBoxedTargetID *targetID, BOOL *stop) {
+ XCTAssertNotNil(self.driver.expectedActiveTargets[targetID],
+ @"Found limbo doc without an expected active target");
+ }];
+
+ for (FSTDocumentKey *expectedLimboDoc in self.driver.expectedLimboDocuments) {
+ XCTAssertNotNil(actualLimboDocs[expectedLimboDoc],
+ @"Expected doc to be in limbo, but was not: %@", expectedLimboDoc);
+ [actualLimboDocs removeObjectForKey:expectedLimboDoc];
+ }
+ XCTAssertTrue(actualLimboDocs.count == 0, "Unexpected docs in limbo: %@", actualLimboDocs);
+}
+
+- (void)validateActiveTargets {
+ // Create a copy so we can modify it in tests
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *actualTargets =
+ [NSMutableDictionary dictionaryWithDictionary:self.driver.activeTargets];
+
+ [self.driver.expectedActiveTargets enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID,
+ FSTQueryData *queryData,
+ BOOL *stop) {
+ XCTAssertNotNil(actualTargets[targetID], @"Expected active target not found: %@", queryData);
+
+ // TODO(mcg): validate the purpose of the target once it's possible to encode that in the
+ // spec tests. For now, only validate properties that can be validated.
+ // XCTAssertEqualObjects(actualTargets[targetID], queryData);
+
+ FSTQueryData *actual = actualTargets[targetID];
+ XCTAssertEqualObjects(actual.query, queryData.query);
+ XCTAssertEqual(actual.targetID, queryData.targetID);
+ XCTAssertEqualObjects(actual.snapshotVersion, queryData.snapshotVersion);
+ XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken);
+
+ [actualTargets removeObjectForKey:targetID];
+ }];
+ XCTAssertTrue(actualTargets.count == 0, "Unexpected active targets: %@", actualTargets);
+}
+
+- (void)runSpecTestSteps:(NSArray *)steps config:(NSDictionary *)config {
+ @try {
+ [self setUpForSpecWithConfig:config];
+ for (NSDictionary *step in steps) {
+ FSTLog(@"Doing step %@", step);
+ [self doStep:step];
+ [self validateStepExpectations:step[@"expect"]];
+ [self validateStateExpectations:step[@"stateExpect"]];
+ }
+ [self.driver validateUsage];
+ } @finally {
+ // Ensure that the driver is torn down even if the test is failing due to a thrown exception so
+ // that any resources held by the driver are released. This is important when the driver is
+ // backed by LevelDB because LevelDB locks its database. If -tearDownForSpec were not called
+ // after an exception then subsequent attempts to open the LevelDB will fail, making it harder
+ // to zero in on the spec tests as a culprit.
+ [self tearDownForSpec];
+ }
+}
+
+#pragma mark - The actual test methods.
+
+- (void)testSpecTests {
+ if ([self isTestBaseClass]) return;
+
+ // Enumerate the .json files containing the spec tests.
+ NSMutableArray<NSString *> *specFiles = [NSMutableArray array];
+ NSMutableArray<NSDictionary *> *parsedSpecs = [NSMutableArray array];
+ NSBundle *bundle = [NSBundle bundleForClass:[self class]];
+ NSFileManager *fs = [NSFileManager defaultManager];
+ BOOL exclusiveMode = NO;
+ for (NSString *file in [fs enumeratorAtPath:[bundle bundlePath]]) {
+ if (![@"json" isEqual:[file pathExtension]]) {
+ continue;
+ }
+
+ // Read and parse the JSON from the file.
+ NSString *fileName = [file stringByDeletingPathExtension];
+ NSString *path = [bundle pathForResource:fileName ofType:@"json"];
+ NSData *json = [NSData dataWithContentsOfFile:path];
+ XCTAssertNotNil(json);
+ NSError *error = nil;
+ id _Nullable parsed = [NSJSONSerialization JSONObjectWithData:json options:0 error:&error];
+ XCTAssertNil(error, @"%@", error);
+ XCTAssertTrue([parsed isKindOfClass:[NSDictionary class]]);
+ NSDictionary *testDict = (NSDictionary *)parsed;
+
+ exclusiveMode = exclusiveMode || [self anyTestsAreMarkedExclusive:testDict];
+ [specFiles addObject:fileName];
+ [parsedSpecs addObject:testDict];
+ }
+
+ // Now iterate over them and run them.
+ __block bool ranAtLeastOneTest = NO;
+ for (NSUInteger i = 0; i < specFiles.count; i++) {
+ NSLog(@"Spec test file: %@", specFiles[i]);
+ // Iterate over the tests in the file and run them.
+ [parsedSpecs[i] enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]);
+ NSDictionary *testDescription = (NSDictionary *)obj;
+ NSString *describeName = testDescription[@"describeName"];
+ NSString *itName = testDescription[@"itName"];
+ NSString *name = [NSString stringWithFormat:@"%@ %@", describeName, itName];
+ NSDictionary *config = testDescription[@"config"];
+ NSArray *steps = testDescription[@"steps"];
+ NSArray<NSString *> *tags = testDescription[@"tags"];
+
+ BOOL runTest = !exclusiveMode || [tags indexOfObject:kExclusiveTag] != NSNotFound;
+ if ([tags indexOfObject:kNoIOSTag] != NSNotFound) {
+ runTest = NO;
+ }
+ if (runTest) {
+ NSLog(@" Spec test: %@", name);
+ [self runSpecTestSteps:steps config:config];
+ ranAtLeastOneTest = YES;
+ } else {
+ NSLog(@" [SKIPPED] Spec test: %@", name);
+ }
+ }];
+ }
+ XCTAssertTrue(ranAtLeastOneTest);
+}
+
+- (BOOL)anyTestsAreMarkedExclusive:(NSDictionary *)tests {
+ __block BOOL found = NO;
+ [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]);
+ NSDictionary *testDescription = (NSDictionary *)obj;
+ NSArray<NSString *> *tags = testDescription[@"tags"];
+ if ([tags indexOfObject:kExclusiveTag] != NSNotFound) {
+ found = YES;
+ *stop = YES;
+ }
+ }];
+ return found;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h
new file mode 100644
index 0000000..0643d76
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "Core/FSTTypes.h"
+
+@class FSTDocumentKey;
+@class FSTMutation;
+@class FSTMutationResult;
+@class FSTQuery;
+@class FSTQueryData;
+@class FSTSnapshotVersion;
+@class FSTUser;
+@class FSTViewSnapshot;
+@class FSTWatchChange;
+@protocol FSTGarbageCollector;
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Interface used for object that contain exactly one of either a view snapshot or an error for the
+ * given query.
+ */
+@interface FSTQueryEvent : NSObject
+@property(nonatomic, strong) FSTQuery *query;
+@property(nonatomic, strong, nullable) FSTViewSnapshot *viewSnapshot;
+@property(nonatomic, strong, nullable) NSError *error;
+@end
+
+/** Holds an outstanding write and its result. */
+@interface FSTOutstandingWrite : NSObject
+/** The write that is outstanding. */
+@property(nonatomic, strong, readwrite) FSTMutation *write;
+/** Whether this write is done (regardless of whether it was successful or not). */
+@property(nonatomic, assign, readwrite) BOOL done;
+/** The error - if any - of this write. */
+@property(nonatomic, strong, nullable, readwrite) NSError *error;
+@end
+
+/** Mapping of user => array of FSTMutations for that user. */
+typedef NSDictionary<FSTUser *, NSArray<FSTOutstandingWrite *> *> FSTOutstandingWriteQueues;
+
+/**
+ * A test driver for FSTSyncEngine that allows simulated event delivery and capture. As much as
+ * possible, all sources of nondeterminism are removed so that test execution is consistent and
+ * reliable.
+ *
+ * FSTSyncEngineTestDriver:
+ *
+ * + constructs an FSTSyncEngine using a mocked FSTDatastore for the backend;
+ * + allows the caller to trigger events (user API calls and incoming FSTDatastore messages);
+ * + performs sequencing validation internally (e.g. that when a user mutation is initiated, the
+ * FSTSyncEngine correctly sends it to the remote store); and
+ * + exposes the set of FSTQueryEvents generated for the caller to verify.
+ *
+ * Events come in three major flavors:
+ *
+ * + user events: simulate user API calls
+ * + watch events: simulate RPC interactions with the Watch backend
+ * + write events: simulate RPC interactions with the Streaming Write backend
+ *
+ * Each method on the driver injects a different event into the system.
+ */
+@interface FSTSyncEngineTestDriver : NSObject
+
+/**
+ * Initializes the underlying FSTSyncEngine with the given local persistence implementation and
+ * garbage collection policy.
+ */
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector;
+
+/**
+ * Initializes the underlying FSTSyncEngine with the given local persistence implementation and
+ * a set of existing outstandingWrites (useful when your FSTPersistence object has
+ * persisted mutation queues).
+ */
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector
+ initialUser:(FSTUser *)initialUser
+ outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites
+ NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** Starts the FSTSyncEngine and its underlying components. */
+- (void)start;
+
+/** Validates that the API has been used correctly after a test is complete. */
+- (void)validateUsage;
+
+/** Shuts the FSTSyncEngine down. */
+- (void)shutdown;
+
+/**
+ * Adds a listener to the FSTSyncEngine as if the user had initiated a new listen for the given
+ * query.
+ *
+ * Resulting events are captured and made available via the capturedEventsSinceLastCall method.
+ *
+ * @param query A valid query to execute against the backend.
+ * @return The target ID assigned by the system to track the query.
+ */
+- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query;
+
+/**
+ * Removes a listener from the FSTSyncEngine as if the user had removed a listener corresponding
+ * to the given query.
+ *
+ * Resulting events are captured and made available via the capturedEventsSinceLastCall method.
+ *
+ * @param query An identical query corresponding to one passed to -addUserListenerWithQuery.
+ */
+- (void)removeUserListenerWithQuery:(FSTQuery *)query;
+
+/**
+ * Delivers a WatchChange RPC to the FSTSyncEngine as if it were received from the backend watch
+ * service, either in response to addUserListener: or removeUserListener calls or because the
+ * simulated backend has new data.
+ *
+ * Resulting events are captured and made available via the capturedEventsSinceLastCall method.
+ *
+ * @param change Any type of watch change
+ * @param snapshot A snapshot version to attach, if applicable. This should be sent when
+ * simulating the server having sent a complete snapshot.
+ */
+- (void)receiveWatchChange:(FSTWatchChange *)change
+ snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot;
+
+/**
+ * Delivers a watch stream error as if the Streaming Watch backend has generated some kind of error.
+ *
+ * @param errorCode A FIRFirestoreErrorCode value, from FIRFirestoreErrors.h
+ * @param userInfo Any additional details that the server might have sent along with the error.
+ * For the moment this is effectively unused, but is logged.
+ */
+- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary<NSString *, id> *)userInfo;
+
+/**
+ * Performs a mutation against the FSTSyncEngine as if the user had written the mutation through
+ * the API.
+ *
+ * Also retains the mutation so that the driver can validate that the sync engine sent the mutation
+ * to the remote store before receiveWatchChange:snapshotVersion: and receiveWriteError:userInfo:
+ * events are processed.
+ *
+ * @param mutation Any type of valid mutation.
+ */
+- (void)writeUserMutation:(FSTMutation *)mutation;
+
+/**
+ * Delivers a write error as if the Streaming Write backend has generated some kind of error.
+ *
+ * For the moment write errors are usually must be in response to a mutation that has been written
+ * with writeUserMutation:. Spontaneously errors due to idle timeout, server restart, or credential
+ * expiration aren't yet supported.
+ *
+ * @param errorCode A FIRFirestoreErrorCode value, from FIRFirestoreErrors.h
+ * @param userInfo Any additional details that the server might have sent along with the error.
+ * For the moment this is effectively unused, but is logged.
+ */
+- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode
+ userInfo:(NSDictionary<NSString *, id> *)userInfo;
+
+/**
+ * Delivers a write acknowledgement as if the Streaming Write backend has acknowledged a write with
+ * the snapshot version at which the write was committed.
+ *
+ * @param commitVersion The snapshot version at which the simulated server has committed
+ * the mutation. Snapshot versions must be monotonically increasing.
+ * @param mutationResults The mutation results for the write that is being acked.
+ */
+- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)mutationResults;
+
+/**
+ * A count of the mutations written to the write stream by the FSTSyncEngine, but not yet
+ * acknowledged via receiveWriteError: or receiveWriteAckWithVersion:mutationResults.
+ */
+@property(nonatomic, readonly) int sentWritesCount;
+
+/**
+ * Switches the FSTSyncEngine to a new user. The test driver tracks the outstanding mutations for
+ * each user, so future receiveWriteAck/Error operations will validate the write sent to the mock
+ * datastore matches the next outstanding write for that user.
+ */
+- (void)changeUser:(FSTUser *)user;
+
+/**
+ * Returns all query events generated by the FSTSyncEngine in response to the event injection
+ * methods called previously. The events are cleared after each invocation of this method.
+ */
+- (NSArray<FSTQueryEvent *> *)capturedEventsSinceLastCall;
+
+/**
+ * The writes that have been sent to the FSTSyncEngine via writeUserMutation: but not yet
+ * acknowledged by calling receiveWriteAck/Error:. They are tracked per-user.
+ *
+ * It is mostly an implementation detail used internally to validate that the writes sent to the
+ * mock backend by the FSTSyncEngine match the user mutations that initiated them.
+ *
+ * It is exposed specifically for use with the
+ * initWithPersistence:GCEnabled:outstandingWrites: initializer to test persistence
+ * scenarios where the FSTSyncEngine is restarted while the FSTPersistence implementation still has
+ * outstanding persisted mutations.
+ *
+ * Note: The size of the list for the current user will generally be the same as
+ * sentWritesCount, but not necessarily, since the FSTRemoteStore limits the number of
+ * outstanding writes to the backend at a given time.
+ */
+@property(nonatomic, strong, readonly) FSTOutstandingWriteQueues *outstandingWrites;
+
+/** The current user for the FSTSyncEngine; determines which mutation queue is active. */
+@property(nonatomic, strong, readonly) FSTUser *currentUser;
+
+/** The current set of documents in limbo. */
+@property(nonatomic, strong, readonly)
+ NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *currentLimboDocuments;
+
+/** The expected set of documents in limbo. */
+@property(nonatomic, strong, readwrite) NSSet<FSTDocumentKey *> *expectedLimboDocuments;
+
+/** The set of active targets as observed on the watch stream. */
+@property(nonatomic, strong, readonly)
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *activeTargets;
+
+/** The expected set of active targets, keyed by target ID. */
+@property(nonatomic, strong, readwrite)
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *expectedActiveTargets;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m
new file mode 100644
index 0000000..b4c0b02
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTSyncEngineTestDriver.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+#import "Auth/FSTUser.h"
+#import "Core/FSTEventManager.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTSyncEngine.h"
+#import "Firestore/FIRFirestoreErrors.h"
+#import "Local/FSTLocalStore.h"
+#import "Local/FSTPersistence.h"
+#import "Model/FSTMutation.h"
+#import "Remote/FSTDatastore.h"
+#import "Remote/FSTWatchChange.h"
+#import "Util/FSTAssert.h"
+#import "Util/FSTDispatchQueue.h"
+#import "Util/FSTLogger.h"
+
+#import "FSTMockDatastore.h"
+#import "FSTSyncEngine+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTQueryEvent
+
+- (NSString *)description {
+ // The Query is also included in the view, so we skip it.
+ return [NSString stringWithFormat:@"<FSTQueryEvent: viewSnapshot=%@, error=%@>",
+ self.viewSnapshot, self.error];
+}
+
+@end
+
+@implementation FSTOutstandingWrite
+@end
+
+@interface FSTSyncEngineTestDriver ()
+
+#pragma mark - Parts of the Firestore system that the spec tests need to control.
+
+@property(nonatomic, strong, readonly) FSTMockDatastore *datastore;
+@property(nonatomic, strong, readonly) FSTEventManager *eventManager;
+@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore;
+@property(nonatomic, strong, readonly) FSTLocalStore *localStore;
+@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine;
+
+#pragma mark - Data structures for holding events sent by the watch stream.
+
+/** A block for the FSTEventAggregator to use to report events to the test. */
+@property(nonatomic, strong, readonly) void (^eventHandler)(FSTQueryEvent *);
+/** The events received by our eventHandler and not yet retrieved via capturedEventsSinceLastCall */
+@property(nonatomic, strong, readonly) NSMutableArray<FSTQueryEvent *> *events;
+/** A dictionary for tracking the listens on queries. */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTQuery *, FSTQueryListener *> *queryListeners;
+
+#pragma mark - Other data structures.
+@property(nonatomic, strong, readwrite) FSTUser *currentUser;
+
+@end
+
+@implementation FSTSyncEngineTestDriver {
+ // ivar is declared as mutable.
+ NSMutableDictionary<FSTUser *, NSMutableArray<FSTOutstandingWrite *> *> *_outstandingWrites;
+}
+
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector {
+ return [self initWithPersistence:persistence
+ garbageCollector:garbageCollector
+ initialUser:[FSTUser unauthenticatedUser]
+ outstandingWrites:@{}];
+}
+
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector
+ initialUser:(FSTUser *)initialUser
+ outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites {
+ if (self = [super init]) {
+ // Create mutable copy of outstandingWrites.
+ _outstandingWrites = [NSMutableDictionary dictionary];
+ [outstandingWrites enumerateKeysAndObjectsUsingBlock:^(
+ FSTUser *user, NSArray<FSTOutstandingWrite *> *writes, BOOL *stop) {
+ _outstandingWrites[user] = [writes mutableCopy];
+ }];
+
+ _events = [NSMutableArray array];
+
+ // Set up the sync engine and various stores.
+ dispatch_queue_t mainQueue = dispatch_get_main_queue();
+ FSTDispatchQueue *dispatchQueue = [FSTDispatchQueue queueWith:mainQueue];
+ _localStore = [[FSTLocalStore alloc] initWithPersistence:persistence
+ garbageCollector:garbageCollector
+ initialUser:initialUser];
+ _datastore = [FSTMockDatastore mockDatastoreWithWorkerDispatchQueue:dispatchQueue];
+
+ _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore];
+
+ _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore
+ remoteStore:_remoteStore
+ initialUser:initialUser];
+ _remoteStore.syncEngine = _syncEngine;
+ _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine];
+
+ _remoteStore.onlineStateDelegate = _eventManager;
+
+ // Set up internal event tracking for the spec tests.
+ NSMutableArray<FSTQueryEvent *> *events = [NSMutableArray array];
+ _eventHandler = ^(FSTQueryEvent *e) {
+ [events addObject:e];
+ };
+ _events = events;
+
+ _queryListeners = [NSMutableDictionary dictionary];
+
+ _expectedLimboDocuments = [NSSet set];
+
+ _expectedActiveTargets = [NSDictionary dictionary];
+
+ _currentUser = initialUser;
+ }
+ return self;
+}
+
+- (void)start {
+ [self.localStore start];
+ [self.remoteStore start];
+}
+
+- (void)validateUsage {
+ // We could relax this if we found a reason to.
+ FSTAssert(self.events.count == 0,
+ @"You must clear all pending events by calling"
+ " capturedEventsSinceLastCall before calling shutdown.");
+}
+
+- (void)shutdown {
+ [self.remoteStore shutdown];
+ [self.localStore shutdown];
+}
+
+- (void)validateNextWriteSent:(FSTMutation *)expectedWrite {
+ NSArray<FSTMutation *> *request = [self.datastore nextSentWrite];
+ // Make sure the write went through the pipe like we expected it to.
+ FSTAssert(request.count == 1, @"Only single mutation requests are supported at the moment");
+ FSTMutation *actualWrite = request[0];
+ FSTAssert([actualWrite isEqual:expectedWrite],
+ @"Mock datastore received write %@ but first outstanding mutation was %@", actualWrite,
+ expectedWrite);
+ FSTLog(@"A write was sent: %@", actualWrite);
+}
+
+- (int)sentWritesCount {
+ return [self.datastore writesSent];
+}
+
+- (void)changeUser:(FSTUser *)user {
+ self.currentUser = user;
+ [self.syncEngine userDidChange:user];
+}
+
+- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:
+ (NSArray<FSTMutationResult *> *)mutationResults {
+ FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject;
+ [[self currentOutstandingWrites] removeObjectAtIndex:0];
+ [self validateNextWriteSent:write.write];
+
+ [self.datastore ackWriteWithVersion:commitVersion mutationResults:mutationResults];
+
+ return write;
+}
+
+- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode
+ userInfo:(NSDictionary<NSString *, id> *)userInfo {
+ NSError *error =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo];
+
+ FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject;
+ [self validateNextWriteSent:write.write];
+
+ // If this is a permanent error, the mutation is not expected to be sent again so we remove it
+ // from currentOutstandingWrites.
+ if ([FSTDatastore isPermanentWriteError:error]) {
+ [[self currentOutstandingWrites] removeObjectAtIndex:0];
+ }
+
+ FSTLog(@"Failing a write.");
+ [self.datastore failWriteWithError:error];
+
+ return write;
+}
+
+- (NSArray<FSTQueryEvent *> *)capturedEventsSinceLastCall {
+ NSArray<FSTQueryEvent *> *result = [self.events copy];
+ [self.events removeAllObjects];
+ return result;
+}
+
+- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query {
+ // TODO(dimond): Allow customizing listen options in spec tests
+ // TODO(dimond): Change spec tests to verify isFromCache on snapshots
+ FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES
+ includeDocumentMetadataChanges:YES
+ waitForSyncWhenOnline:NO];
+ FSTQueryListener *listener = [[FSTQueryListener alloc]
+ initWithQuery:query
+ options:options
+ viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ FSTQueryEvent *event = [[FSTQueryEvent alloc] init];
+ event.query = query;
+ event.viewSnapshot = snapshot;
+ event.error = error;
+ [self.events addObject:event];
+ }];
+ self.queryListeners[query] = listener;
+ return [self.eventManager addListener:listener];
+}
+
+- (void)removeUserListenerWithQuery:(FSTQuery *)query {
+ FSTQueryListener *listener = self.queryListeners[query];
+ [self.queryListeners removeObjectForKey:query];
+ [self.eventManager removeListener:listener];
+}
+
+- (void)writeUserMutation:(FSTMutation *)mutation {
+ FSTOutstandingWrite *write = [[FSTOutstandingWrite alloc] init];
+ write.write = mutation;
+ [[self currentOutstandingWrites] addObject:write];
+ FSTLog(@"sending a user write.");
+ [self.syncEngine writeMutations:@[ mutation ]
+ completion:^(NSError *_Nullable error) {
+ FSTLog(@"A callback was called with error: %@", error);
+ write.done = YES;
+ write.error = error;
+ }];
+}
+
+- (void)receiveWatchChange:(FSTWatchChange *)change
+ snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot {
+ [self.datastore writeWatchChange:change snapshotVersion:snapshot];
+}
+
+- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary<NSString *, id> *)userInfo {
+ NSError *error =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo];
+
+ [self.datastore failWatchStreamWithError:error];
+ // Unlike web, stream should re-open synchronously
+ FSTAssert(self.datastore.isWatchStreamOpen, @"Watch stream is open");
+}
+
+- (NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments {
+ return [self.syncEngine currentLimboDocuments];
+}
+
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)activeTargets {
+ return [[self.datastore activeTargets] copy];
+}
+
+#pragma mark - Helper Methods
+
+- (NSMutableArray<FSTOutstandingWrite *> *)currentOutstandingWrites {
+ NSMutableArray<FSTOutstandingWrite *> *writes = _outstandingWrites[self.currentUser];
+ if (!writes) {
+ writes = [NSMutableArray array];
+ _outstandingWrites[self.currentUser] = writes;
+ }
+ return writes;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/json/README.md b/Firestore/Example/Tests/SpecTests/json/README.md
new file mode 100644
index 0000000..bcc9b38
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/README.md
@@ -0,0 +1,3 @@
+These json files are generated from the web test sources.
+
+TODO(mikelehen): Re-add instructions for generating these.
diff --git a/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json
new file mode 100644
index 0000000..ef41afe
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json
@@ -0,0 +1,147 @@
+{
+ "Events are raised after watch ack": {
+ "describeName": "Collections:",
+ "itName": "Events are raised after watch ack",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Events are raised for local sets before watch ack": {
+ "describeName": "Collections:",
+ "itName": "Events are raised for local sets before watch ack",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
new file mode 100644
index 0000000..ab42241
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
@@ -0,0 +1,738 @@
+{
+ "Existence filter mismatch triggers re-run of query": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter mismatch triggers re-run of query",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/2"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Existence filter mismatch will drop resume token": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter mismatch will drop resume token",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "existence-filter-resume-token"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "existence-filter-resume-token"
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/2"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Existence filter limbo resolution is denied": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter limbo resolution is denied",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/2"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 1
+ ],
+ "cause": {
+ "code": 7
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ },
+ "limboDocs": []
+ },
+ "watchSnapshot": 3000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
new file mode 100644
index 0000000..ee2d883
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
@@ -0,0 +1,1150 @@
+{
+ "Limbo documents are deleted without an existence filter": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents are deleted without an existence filter",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-2"
+ ],
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Limbo documents are deleted with an existence filter": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents are deleted with an existence filter",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Limbo documents are resolved with updates": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents are resolved with updates",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 1
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Limbo documents are resolved with updates in different snapshot than \"current\"": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents are resolved with updates in different snapshot than \"current\"",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 1,
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-1003"
+ ],
+ "watchSnapshot": 1003
+ }
+ ]
+ },
+ "Document remove message will cause docs to go in limbo": {
+ "describeName": "Limbo Documents:",
+ "itName": "Document remove message will cause docs to go in limbo",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "key": "collection/b",
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1003,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/b"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/b",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-1004"
+ ],
+ "watchSnapshot": 1004,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
new file mode 100644
index 0000000..5a02463
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
@@ -0,0 +1,1626 @@
+{
+ "Documents in limit are replaced by remote event": {
+ "describeName": "Limits:",
+ "itName": "Documents in limit are replaced by remote event",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1002,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Deleted Document in limbo in full limit query": {
+ "describeName": "Limits:",
+ "itName": "Deleted Document in limbo in full limit query",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ],
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Documents in limit can handle removed messages": {
+ "describeName": "Limits:",
+ "itName": "Documents in limit can handle removed messages",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "key": "collection/c",
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Documents in limit are can handle removed messages for only one of many query": {
+ "describeName": "Limits:",
+ "itName": "Documents in limit are can handle removed messages for only one of many query",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2,
+ 4
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "key": "collection/c",
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Multiple docs in limbo in full limit query": {
+ "describeName": "Limits:",
+ "itName": "Multiple docs in limbo in full limit query",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ],
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ],
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ],
+ [
+ "collection/e",
+ 1004,
+ {
+ "key": "e"
+ }
+ ],
+ [
+ "collection/f",
+ 1005,
+ {
+ "key": "f"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-1005"
+ ],
+ "watchSnapshot": 1005,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ],
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ],
+ [
+ "collection/e",
+ 1004,
+ {
+ "key": "e"
+ }
+ ],
+ [
+ "collection/f",
+ 1005,
+ {
+ "key": "f"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/e",
+ 1004,
+ {
+ "key": "e"
+ }
+ ],
+ [
+ "collection/f",
+ 1005,
+ {
+ "key": "f"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a",
+ "collection/b"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "3": {
+ "query": {
+ "path": "collection/b",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/b",
+ "collection/c"
+ ],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "3": {
+ "query": {
+ "path": "collection/b",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "5": {
+ "query": {
+ "path": "collection/c",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 1
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 3
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 3
+ ],
+ "resume-token-2001"
+ ],
+ "watchSnapshot": 2001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/c",
+ "collection/d"
+ ],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "5": {
+ "query": {
+ "path": "collection/c",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "7": {
+ "query": {
+ "path": "collection/d",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 3
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 5
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 5
+ ],
+ "resume-token-2002"
+ ],
+ "watchSnapshot": 2002,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/d"
+ ],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "7": {
+ "query": {
+ "path": "collection/d",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/e",
+ 1004,
+ {
+ "key": "e"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 5
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 7
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 7
+ ],
+ "resume-token-2003"
+ ],
+ "watchSnapshot": 2003,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/f",
+ 1005,
+ {
+ "key": "f"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 7
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
new file mode 100644
index 0000000..35704f2
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
@@ -0,0 +1,1524 @@
+{
+ "Contents of query are cleared when listen is removed.": {
+ "describeName": "Listens:",
+ "itName": "Contents of query are cleared when listen is removed.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ }
+ ]
+ },
+ "Contents of query update when new data is received.": {
+ "describeName": "Listens:",
+ "itName": "Contents of query update when new data is received.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Ensure correct query results with latency-compensated deletes": {
+ "describeName": "Listens:",
+ "itName": "Ensure correct query results with latency-compensated deletes",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userDelete": "collection/b"
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "a": true
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "b": true
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "a": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "limit": 10,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 10,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 10,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "a": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Will process removals without waiting for a consistent snapshot": {
+ "describeName": "Listens:",
+ "itName": "Will process removals without waiting for a consistent snapshot",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ],
+ "cause": {
+ "code": 8
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {}
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 8,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Will gracefully process failed targets": {
+ "describeName": "Listens:",
+ "itName": "Will gracefully process failed targets",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection1/a",
+ 1000,
+ {
+ "a": true
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection2/a",
+ 1001,
+ {
+ "b": true
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ],
+ "cause": {
+ "code": 8
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 8,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection2/a",
+ 1001,
+ {
+ "b": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Will gracefully handle watch stream reverting snapshots": {
+ "describeName": "Listens:",
+ "itName": "Will gracefully handle watch stream reverting snapshots",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Will gracefully handle watch stream reverting snapshots (with restart)": {
+ "describeName": "Listens:",
+ "itName": "Will gracefully handle watch stream reverting snapshots (with restart)",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": []
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Individual documents cannot revert": {
+ "describeName": "Listens:",
+ "itName": "Individual documents cannot revert",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000",
+ "visible": true
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000",
+ "visible": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000",
+ "visible": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 3000,
+ {
+ "v": "v3000",
+ "visible": false
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-4000"
+ ],
+ "watchSnapshot": 4000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 3000,
+ {
+ "v": "v3000",
+ "visible": false
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 4
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000",
+ "visible": false
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-5000"
+ ],
+ "watchSnapshot": 5000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-4000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 3000,
+ {
+ "v": "v3000",
+ "visible": false
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-6000"
+ ],
+ "watchSnapshot": 6000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json
new file mode 100644
index 0000000..e58bae1
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json
@@ -0,0 +1,151 @@
+{
+ "Empty queries are resolved if client goes offline": {
+ "describeName": "Offline:",
+ "itName": "Empty queries are resolved if client goes offline",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ }
+ ]
+ },
+ "A successful message delays offline status": {
+ "describeName": "Offline:",
+ "itName": "A successful message delays offline status",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json
new file mode 100644
index 0000000..1009206
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json
@@ -0,0 +1,155 @@
+{
+ "orderBy applies filtering based on local state": {
+ "describeName": "OrderBy:",
+ "itName": "orderBy applies filtering based on local state",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "key": "a",
+ "sort": 1
+ }
+ ]
+ },
+ {
+ "userPatch": [
+ "collection/b",
+ {
+ "sort": 2
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/c",
+ {
+ "key": "b"
+ }
+ ]
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": [
+ [
+ "sort",
+ "asc"
+ ]
+ ]
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": [
+ [
+ "sort",
+ "asc"
+ ]
+ ]
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": [
+ [
+ "sort",
+ "asc"
+ ]
+ ]
+ },
+ "added": [
+ [
+ "collection/a",
+ 0,
+ {
+ "key": "a",
+ "sort": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": [
+ [
+ "sort",
+ "asc"
+ ]
+ ]
+ },
+ "added": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b",
+ "sort": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json
new file mode 100644
index 0000000..158e337
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json
@@ -0,0 +1,858 @@
+{
+ "Local mutations are persisted and re-sent": {
+ "describeName": "Persistence:",
+ "itName": "Local mutations are persisted and re-sent",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "collection/key1",
+ {
+ "foo": "bar"
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key2",
+ {
+ "baz": "quu"
+ }
+ ]
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": [],
+ "numOutstandingWrites": 2
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1,
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 2,
+ "expectUserCallback": false
+ },
+ "stateExpect": {
+ "numOutstandingWrites": 0
+ }
+ }
+ ]
+ },
+ "Persisted local mutations are visible to listeners": {
+ "describeName": "Persistence:",
+ "itName": "Persisted local mutations are visible to listeners",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "collection/key1",
+ {
+ "foo": "bar"
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key2",
+ {
+ "baz": "quu"
+ }
+ ]
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": []
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key1",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ],
+ [
+ "collection/key2",
+ 0,
+ {
+ "baz": "quu"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ }
+ ]
+ },
+ "Remote documents are persisted": {
+ "describeName": "Persistence:",
+ "itName": "Remote documents are persisted",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": []
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Remote documents from watch are not GC'd": {
+ "describeName": "Persistence:",
+ "itName": "Remote documents from watch are not GC'd",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Remote documents from user sets are not GC'd": {
+ "describeName": "Persistence:",
+ "itName": "Remote documents from user sets are not GC'd",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Mutation Queue is persisted across uid switches": {
+ "describeName": "Persistence:",
+ "itName": "Mutation Queue is persisted across uid switches",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "users/anon",
+ {
+ "uid": "anon"
+ }
+ ]
+ },
+ {
+ "changeUser": "user1",
+ "stateExpect": {
+ "numOutstandingWrites": 0
+ }
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1"
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1",
+ "extra": true
+ }
+ ]
+ },
+ {
+ "changeUser": null,
+ "stateExpect": {
+ "numOutstandingWrites": 1
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "changeUser": "user1",
+ "stateExpect": {
+ "numOutstandingWrites": 2
+ }
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": true
+ }
+ }
+ ]
+ },
+ "Mutation Queue is persisted across uid switches (with restarts)": {
+ "describeName": "Persistence:",
+ "itName": "Mutation Queue is persisted across uid switches (with restarts)",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "users/anon",
+ {
+ "uid": "anon"
+ }
+ ]
+ },
+ {
+ "changeUser": "user1",
+ "stateExpect": {
+ "numOutstandingWrites": 0
+ }
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1"
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1",
+ "extra": true
+ }
+ ]
+ },
+ {
+ "changeUser": null
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": [],
+ "numOutstandingWrites": 1
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": false
+ }
+ },
+ {
+ "changeUser": "user1"
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": [],
+ "numOutstandingWrites": 2
+ }
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": false
+ }
+ }
+ ]
+ },
+ "Visible mutations reflect uid switches": {
+ "describeName": "Persistence:",
+ "itName": "Visible mutations reflect uid switches",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "users/existing",
+ 0,
+ {
+ "uid": "existing"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-500"
+ ],
+ "watchSnapshot": 500,
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "users/existing",
+ 0,
+ {
+ "uid": "existing"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "users/anon",
+ {
+ "uid": "anon"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "users/anon",
+ 0,
+ {
+ "uid": "anon"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "changeUser": "user1",
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-500"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "users/anon",
+ 0,
+ {
+ "uid": "anon"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "users/user1",
+ 0,
+ {
+ "uid": "user1"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "changeUser": null,
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "users/anon",
+ 0,
+ {
+ "uid": "anon"
+ },
+ "local"
+ ]
+ ],
+ "removed": [
+ [
+ "users/user1",
+ 0,
+ {
+ "uid": "user1"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json
new file mode 100644
index 0000000..edb0751
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json
@@ -0,0 +1,559 @@
+{
+ "Waits for watch to remove targets": {
+ "describeName": "Remote store:",
+ "itName": "Waits for watch to remove targets",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token"
+ ],
+ "watchSnapshot": 1000
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Waits for watch to ack last target add": {
+ "describeName": "Remote store:",
+ "itName": "Waits for watch to ack last target add",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token"
+ ],
+ "watchSnapshot": 1000
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/c",
+ 1000,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/d",
+ 1000,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/d",
+ 1000,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Cleans up watch state correctly": {
+ "describeName": "Remote store:",
+ "itName": "Cleans up watch state correctly",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json
new file mode 100644
index 0000000..25ea84a
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json
@@ -0,0 +1,250 @@
+{
+ "Resume tokens are sent after watch stream restarts": {
+ "describeName": "Resume tokens:",
+ "itName": "Resume tokens are sent after watch stream restarts",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "custom-query-resume-token"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "custom-query-resume-token"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "Resume tokens are used across new listens": {
+ "describeName": "Resume tokens:",
+ "itName": "Resume tokens are used across new listens",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "custom-query-resume-token"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "custom-query-resume-token"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ],
+ "watchSnapshot": 1001
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/write_spec_test.json b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json
new file mode 100644
index 0000000..60ed107
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json
@@ -0,0 +1,5437 @@
+{
+ "Two sequential writes to different documents smoke test.": {
+ "describeName": "Writes:",
+ "itName": "Two sequential writes to different documents smoke test.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/b",
+ 500,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/b",
+ 500,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/b",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/b",
+ 500,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 2500,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 3000
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/b",
+ 2500,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Event is raised for a local set before and after the write ack": {
+ "describeName": "Writes:",
+ "itName": "Event is raised for a local set before and after the write ack",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Cache will not keep data for an outdated write ack": {
+ "describeName": "Writes:",
+ "itName": "Cache will not keep data for an outdated write ack",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 10000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 10000
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 10000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Cache raises correct event if write is acked before watch delivers it": {
+ "describeName": "Writes:",
+ "itName": "Cache raises correct event if write is acked before watch delivers it",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Cache will hold local write until watch catches up": {
+ "describeName": "Writes:",
+ "itName": "Cache will hold local write until watch catches up",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "v": 3
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 3
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 3000,
+ {
+ "doc": "b"
+ }
+ ],
+ [
+ "collection/key",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 3000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 3000,
+ {
+ "doc": "b"
+ }
+ ]
+ ],
+ "metadata": [
+ [
+ "collection/key",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes are pipelined": {
+ "describeName": "Writes:",
+ "itName": "Writes are pipelined",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token"
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a0",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a0",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a1",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a1",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a2",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a2",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a3",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a3",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a4",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a4",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a5",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a5",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a6",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a6",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a7",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a7",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a8",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a8",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a9",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a9",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a10",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a10",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a11",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a11",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a12",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a12",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a13",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a13",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a14",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a14",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ],
+ "stateExpect": {
+ "numOutstandingWrites": 10
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a0",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a0",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a1",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a1",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a2",
+ 3000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 3000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a2",
+ 3000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 4000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a3",
+ 4000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 4000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a3",
+ 4000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 5000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a4",
+ 5000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 5000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a4",
+ 5000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 6000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a5",
+ 6000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 6000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a5",
+ 6000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 7000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a6",
+ 7000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 7000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a6",
+ 7000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 8000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a7",
+ 8000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 8000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a7",
+ 8000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 9000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a8",
+ 9000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 9000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a8",
+ 9000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 10000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a9",
+ 10000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 10000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a9",
+ 10000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 11000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a10",
+ 11000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 11000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a10",
+ 11000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 12000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a11",
+ 12000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 12000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a11",
+ 12000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 13000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a12",
+ 13000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 13000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a12",
+ 13000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 14000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a13",
+ 14000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 14000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a13",
+ 14000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 15000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a14",
+ 15000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 15000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a14",
+ 15000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Pipelined writes can fail": {
+ "describeName": "Writes:",
+ "itName": "Pipelined writes can fail",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/a0",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a0",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a1",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a1",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a2",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a2",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a3",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a3",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a4",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a4",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a5",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a5",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a6",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a6",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a7",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a7",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a8",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a8",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a9",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a9",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a10",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a10",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a11",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a11",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a12",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a12",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a13",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a13",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a14",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a14",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ],
+ "stateExpect": {
+ "numOutstandingWrites": 10
+ }
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a0",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a1",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a2",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a3",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a4",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a5",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a6",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a7",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a8",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a9",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a10",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a11",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a12",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a13",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a14",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "numOutstandingWrites": 0
+ }
+ }
+ ]
+ },
+ "Failed writes are released immediately.": {
+ "describeName": "Writes:",
+ "itName": "Failed writes are released immediately.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/b",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Held writes are not re-sent.": {
+ "describeName": "Writes:",
+ "itName": "Held writes are not re-sent.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-500"
+ ],
+ "watchSnapshot": 500,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "userSet": [
+ "collection/b",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/b",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/b",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Held writes are released when there are no queries left.": {
+ "describeName": "Writes:",
+ "itName": "Held writes are released when there are no queries left.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-500"
+ ],
+ "watchSnapshot": 500,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ }
+ ]
+ },
+ "Writes that fail with code invalid-argument are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code invalid-argument are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 3
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code not-found are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code not-found are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 5
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code already-exists are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code already-exists are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 6
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code permission-denied are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code permission-denied are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code failed-precondition are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code failed-precondition are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 9
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code aborted are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code aborted are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 10
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code out-of-range are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code out-of-range are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 11
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code unimplemented are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code unimplemented are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 12
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code data-loss are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code data-loss are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 15
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code cancelled are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code cancelled are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 1
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code unknown are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code unknown are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 2
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code deadline-exceeded are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code deadline-exceeded are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 4
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code resource-exhausted are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code resource-exhausted are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 8
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code internal are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code internal are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 13
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code unavailable are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code unavailable are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 14
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code unauthenticated are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code unauthenticated are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 16
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Ensure correct events after patching a doc (including a delete) and getting watcher events.": {
+ "describeName": "Writes:",
+ "itName": "Ensure correct events after patching a doc (including a delete) and getting watcher events.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/doc",
+ 1000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-500"
+ ],
+ "watchSnapshot": 500,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/doc",
+ 1000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userPatch": [
+ "collection/doc",
+ {
+ "v": 2,
+ "a.c": "<DELETE>"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/doc",
+ 1000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/doc",
+ 2000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/doc",
+ 2000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/Tests-Info.plist b/Firestore/Example/Tests/Tests-Info.plist
new file mode 100644
index 0000000..169b6f7
--- /dev/null
+++ b/Firestore/Example/Tests/Tests-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>$(PRODUCT_BUNDLE_IDENTIFIER)</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/Firestore/Example/Tests/Util/FSTAssertTests.m b/Firestore/Example/Tests/Util/FSTAssertTests.m
new file mode 100644
index 0000000..f0734df
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTAssertTests.m
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Util/FSTAssert.h"
+
+#import <XCTest/XCTest.h>
+
+@interface FSTAssertTests : XCTestCase
+@end
+
+@implementation FSTAssertTests
+
+- (void)testFail {
+ @try {
+ [self failingMethod];
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar");
+ }
+}
+
+// A method guaranteed to fail. Note that the return type is intentionally something that is
+// not actually returned in the body to ensure that the function attribute declarations in the
+// macro properly mark this macro invocation as never returning.
+- (int)failingMethod {
+ FSTFail(@"%d:%s:%@", 0, "foo", @"bar");
+}
+
+- (void)testCFail {
+ @try {
+ failingFunction();
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar");
+ }
+}
+
+// A function guaranteed to fail. Note that the return type is intentionally something that is
+// not actually returned in the body to ensure that the function attribute declarations in the
+// macro properly mark this macro invocation as never returning.
+int failingFunction() {
+ FSTCFail(@"%d:%s:%@", 0, "foo", @"bar");
+}
+
+- (void)testAssertNonFailing {
+ @try {
+ FSTAssert(YES, @"shouldn't fail");
+ } @catch (NSException *ex) {
+ XCTFail("Should not have failed, but got %@", ex);
+ }
+}
+
+- (void)testAssertFailing {
+ @try {
+ FSTAssert(NO, @"should fail");
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail");
+ }
+}
+
+- (void)testCAssertNonFailing {
+ @try {
+ nonAssertingFunction();
+ } @catch (NSException *ex) {
+ XCTFail("Should not have failed, but got %@", ex);
+ }
+}
+
+int nonAssertingFunction() {
+ FSTCAssert(YES, @"shouldn't fail");
+ return 0;
+}
+
+- (void)testCAssertFailing {
+ @try {
+ assertingFunction();
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail");
+ }
+}
+
+int assertingFunction() {
+ FSTCAssert(NO, @"should fail");
+}
+
+@end
diff --git a/Firestore/Example/Tests/Util/FSTComparisonTests.m b/Firestore/Example/Tests/Util/FSTComparisonTests.m
new file mode 100644
index 0000000..9728f20
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTComparisonTests.m
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Util/FSTComparison.h"
+
+#import <XCTest/XCTest.h>
+
+union DoubleBits {
+ double d;
+ uint64_t bits;
+};
+
+#define ASSERT_BIT_EQUALS(expected, actual) \
+ do { \
+ union DoubleBits expectedBits = {.d = expected}; \
+ union DoubleBits actualBits = {.d = expected}; \
+ if (expectedBits.bits != actualBits.bits) { \
+ XCTFail(@"Expected <%f> to compare equal to <%f> with bits <%llX> equal to <%llX>", actual, \
+ expected, actualBits.bits, expectedBits.bits); \
+ } \
+ } while (0);
+
+#define ASSERT_ORDERED_SAME(doubleValue, longValue) \
+ do { \
+ NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \
+ if (result != NSOrderedSame) { \
+ XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \
+ } \
+ } while (0);
+
+#define ASSERT_ORDERED_DESCENDING(doubleValue, longValue) \
+ do { \
+ NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \
+ if (result != NSOrderedDescending) { \
+ XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \
+ } \
+ } while (0);
+
+#define ASSERT_ORDERED_ASCENDING(doubleValue, longValue) \
+ do { \
+ NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \
+ if (result != NSOrderedAscending) { \
+ XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \
+ } \
+ } while (0);
+
+@interface FSTComparisonTests : XCTestCase
+@end
+
+@implementation FSTComparisonTests
+
+- (void)testMixedComparison {
+ // Infinities
+ ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MIN);
+ ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MAX);
+ ASSERT_ORDERED_ASCENDING(-INFINITY, 0LL);
+
+ ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MIN);
+ ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MAX);
+ ASSERT_ORDERED_DESCENDING(INFINITY, 0LL);
+
+ // NaN
+ ASSERT_ORDERED_ASCENDING(NAN, LLONG_MIN);
+ ASSERT_ORDERED_ASCENDING(NAN, LLONG_MAX);
+ ASSERT_ORDERED_ASCENDING(NAN, 0LL);
+
+ // Large values (note DBL_MIN is positive and near zero).
+ ASSERT_ORDERED_ASCENDING(-DBL_MAX, LLONG_MIN);
+
+ // Tests around LLONG_MIN
+ ASSERT_BIT_EQUALS((double)LLONG_MIN, -0x1.0p63);
+ ASSERT_ORDERED_SAME(-0x1.0p63, LLONG_MIN);
+ ASSERT_ORDERED_ASCENDING(-0x1.0p63, LLONG_MIN + 1);
+
+ XCTAssertLessThan(-0x1.0000000000001p63, -0x1.0p63);
+ ASSERT_ORDERED_ASCENDING(-0x1.0000000000001p63, LLONG_MIN);
+ ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp62, LLONG_MIN);
+
+ // Tests around LLONG_MAX
+ // Note LLONG_MAX cannot be exactly represented by a double, so the system rounds it to the
+ // nearest double, which is 2^63. This number, in turn is larger than the maximum representable
+ // as a long.
+ ASSERT_BIT_EQUALS(0x1.0p63, (double)LLONG_MAX);
+ ASSERT_ORDERED_DESCENDING(0x1.0p63, LLONG_MAX);
+
+ // The largest value with an exactly long representation
+ XCTAssertEqual((long)0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL);
+ ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL);
+
+ ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFB00LL);
+ ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFBFFLL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC01LL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFD00LL);
+
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp62, 0x7FFFFFFFFFFFFC00LL);
+
+ // Tests around MAX_SAFE_INTEGER
+ ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFELL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp52, 0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp52, 0x20000000000000LL);
+
+ // Tests around MIN_SAFE_INTEGER
+ ASSERT_ORDERED_SAME(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_ASCENDING(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFELL);
+ ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFEp52, -0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp52, -0x20000000000000LL);
+
+ // Tests around zero.
+ ASSERT_ORDERED_SAME(-0.0, 0LL);
+ ASSERT_ORDERED_SAME(0.0, 0LL);
+
+ // The smallest representable positive value should be greater than zero
+ ASSERT_ORDERED_DESCENDING(DBL_MIN, 0LL);
+ ASSERT_ORDERED_ASCENDING(-DBL_MIN, 0LL);
+
+ // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal
+ // number: <https://en.wikipedia.org/wiki/Denormal_number>.
+ double minSubNormal = 0x1.0p-1074;
+ ASSERT_ORDERED_DESCENDING(minSubNormal, 0LL);
+ ASSERT_ORDERED_ASCENDING(-minSubNormal, 0LL);
+
+ // Other sanity checks
+ ASSERT_ORDERED_ASCENDING(0.5, 1LL);
+ ASSERT_ORDERED_DESCENDING(0.5, 0LL);
+ ASSERT_ORDERED_ASCENDING(1.5, 2LL);
+ ASSERT_ORDERED_DESCENDING(1.5, 1LL);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.h b/Firestore/Example/Tests/Util/FSTEventAccumulator.h
new file mode 100644
index 0000000..ae5392c
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.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 FIRDocumentSnapshot;
+@class FIRQuerySnapshot;
+@class XCTestCase;
+@class XCTestExpectation;
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef void (^FSTGenericEventHandler)(id _Nullable, NSError *error);
+
+@interface FSTEventAccumulator : NSObject
+
++ (instancetype)accumulatorForTest:(XCTestCase *)testCase;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (id)awaitEventWithName:(NSString *)name;
+
+- (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name;
+
+@property(nonatomic, strong, readonly) FSTGenericEventHandler handler;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.m b/Firestore/Example/Tests/Util/FSTEventAccumulator.m
new file mode 100644
index 0000000..c7e5b41
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.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 "FSTEventAccumulator.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Util/FSTAssert.h"
+
+#import "XCTestCase+Await.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTEventAccumulator ()
+- (instancetype)initForTest:(XCTestCase *)testCase NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, weak, readonly) XCTestCase *testCase;
+
+@property(nonatomic, assign) NSUInteger maxEvents;
+@property(nonatomic, strong, nullable) XCTestExpectation *expectation;
+@end
+
+@implementation FSTEventAccumulator {
+ NSMutableArray<id> *_events;
+}
+
++ (instancetype)accumulatorForTest:(XCTestCase *)testCase {
+ return [[FSTEventAccumulator alloc] initForTest:testCase];
+}
+
+- (instancetype)initForTest:(XCTestCase *)testCase {
+ if (self = [super init]) {
+ _testCase = testCase;
+ _events = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name {
+ @synchronized(self) {
+ FSTAssert(!self.expectation, @"Existing expectation still pending?");
+ self.expectation = [self.testCase expectationWithDescription:name];
+ self.maxEvents = self.maxEvents + events;
+ [self checkFulfilled];
+ }
+
+ // Don't await within @synchronized block to avoid deadlocking.
+ [self.testCase awaitExpectations];
+
+ return [_events subarrayWithRange:NSMakeRange(self.maxEvents - events, events)];
+}
+
+- (id)awaitEventWithName:(NSString *)name {
+ NSArray<id> *events = [self awaitEvents:1 name:name];
+ return events[0];
+}
+
+// Overrides the handler property
+- (void (^)(id _Nullable, NSError *))handler {
+ return ^void(id _Nullable value, NSError *error) {
+ // We can't store nil in the _events array, but these are still interesting to tests so store
+ // NSNull instead.
+ id event = value ? value : [NSNull null];
+
+ @synchronized(self) {
+ [_events addObject:event];
+ [self checkFulfilled];
+ }
+ };
+}
+
+- (void)checkFulfilled {
+ if (_events.count >= self.maxEvents) {
+ [self.expectation fulfill];
+ self.expectation = nil;
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h
new file mode 100644
index 0000000..3facc9c
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTHelpers.h
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "API/FIRDocumentReference+Internal.h"
+#import "Core/FSTTypes.h"
+#import "Model/FSTDocumentDictionary.h"
+#import "Model/FSTDocumentKeySet.h"
+
+@class FIRGeoPoint;
+@class FSTDeleteMutation;
+@class FSTDeletedDocument;
+@class FSTDocument;
+@class FSTDocumentKeyReference;
+@class FSTDocumentSet;
+@class FSTFieldPath;
+@class FSTFieldValue;
+@class FSTLocalViewChanges;
+@class FSTPatchMutation;
+@class FSTQuery;
+@class FSTRemoteEvent;
+@class FSTResourceName;
+@class FSTResourcePath;
+@class FSTSetMutation;
+@class FSTSnapshotVersion;
+@class FSTSortOrder;
+@class FSTTargetChange;
+@class FSTTimestamp;
+@class FSTTransformMutation;
+@class FSTView;
+@class FSTViewSnapshot;
+@class FSTObjectValue;
+@protocol FSTFilter;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#if __cplusplus
+extern "C" {
+#endif
+
+#define FSTAssertIsKindOfClass(value, classType) \
+ do { \
+ XCTAssertEqualObjects([value class], [classType class]); \
+ } while (0);
+
+/**
+ * Asserts that the given NSSet of FSTDocumentKeys contains exactly the given expected keys.
+ * This is a macro instead of a method so that the failure shows up on the right line.
+ *
+ * @param actualSet An NSSet of FSTDocumentKeys.
+ * @param expectedArray A sorted array of keys that actualSet must be equal to (after converting
+ * to an array and sorting).
+ */
+#define FSTAssertEqualSets(actualSet, expectedArray) \
+ do { \
+ NSArray<FSTDocumentKey *> *actual = [(actualSet)allObjects]; \
+ actual = [actual sortedArrayUsingSelector:@selector(compare:)]; \
+ XCTAssertEqualObjects(actual, (expectedArray)); \
+ } while (0)
+
+/**
+ * Takes an array of "equality group" arrays and asserts that the compare: selector returns the
+ * same as compare: on the indexes of the "equality groups" (NSOrderedSame for items in the same
+ * group).
+ */
+#define FSTAssertComparisons(values) \
+ do { \
+ for (NSUInteger i = 0; i < [values count]; i++) { \
+ for (id left in values[i]) { \
+ for (NSUInteger j = 0; j < [values count]; j++) { \
+ for (id right in values[j]) { \
+ NSComparisonResult expected = [@(i) compare:@(j)]; \
+ NSComparisonResult result = [left compare:right]; \
+ NSComparisonResult inverseResult = [right compare:left]; \
+ XCTAssertEqual(result, expected, @"comparing %@ with %@ at (%lu, %lu)", left, right, \
+ i, j); \
+ XCTAssertEqual(inverseResult, -expected, @"comparing %@ with %@ at (%lu, %lu)", right, \
+ left, j, i); \
+ } \
+ } \
+ } \
+ } \
+ } while (0)
+
+/**
+ * Takes an array of "equality group" arrays and asserts that the isEqual: selector returns TRUE
+ * if-and-only-if items are in the same group.
+ *
+ * Additionally checks that the hash: selector returns the same value for items in the same group.
+ */
+#define FSTAssertEqualityGroups(values) \
+ do { \
+ for (NSUInteger i = 0; i < [values count]; i++) { \
+ for (id left in values[i]) { \
+ for (NSUInteger j = 0; j < [values count]; j++) { \
+ for (id right in values[j]) { \
+ if (i == j) { \
+ XCTAssertEqualObjects(left, right); \
+ XCTAssertEqual([left hash], [right hash], @"comparing hash of %@ with hash of %@", \
+ left, right); \
+ } else { \
+ XCTAssertNotEqualObjects(left, right); \
+ } \
+ } \
+ } \
+ } \
+ } \
+ } while (0)
+
+// Helper for validating API exceptions.
+#define FSTAssertThrows(expression, exceptionReason, ...) \
+ ({ \
+ BOOL __didThrow = NO; \
+ @try { \
+ (void)(expression); \
+ } @catch (NSException * exception) { \
+ __didThrow = YES; \
+ XCTAssertEqualObjects(exception.reason, exceptionReason); \
+ } \
+ XCTAssertTrue(__didThrow, ##__VA_ARGS__); \
+ })
+
+/** Creates a new FSTTimestamp from components. Note that year, month, and day are all one-based. */
+FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second);
+
+/** Creates a new NSDate from components. Note that year, month, and day are all one-based. */
+NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second);
+
+/**
+ * Creates a new NSData from the var args of bytes, must be terminated with a negative byte
+ */
+NSData *FSTTestData(int bytes, ...);
+
+/** Creates a new GeoPoint from the latitude and longitude values */
+FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude);
+
+/**
+ * Creates a new NSDateComponents from components. Note that year, month, and day are all
+ * one-based.
+ */
+NSDateComponents *FSTTestDateComponents(
+ int year, int month, int day, int hour, int minute, int second);
+
+FSTFieldPath *FSTTestFieldPath(NSString *field);
+
+/** Wraps a plain value into an FSTFieldValue instance. */
+FSTFieldValue *FSTTestFieldValue(id _Nullable value);
+
+/** Wraps a NSDictionary value into an FSTObjectValue instance. */
+FSTObjectValue *FSTTestObjectValue(NSDictionary<NSString *, id> *data);
+
+/** A convenience method for creating document keys for tests. */
+FSTDocumentKey *FSTTestDocKey(NSString *path);
+
+/** A convenience method for creating a document key set for tests. */
+FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys);
+
+/** Allow tests to just use an int literal for versions. */
+typedef int64_t FSTTestSnapshotVersion;
+
+/** A convenience method for creating snapshot versions for tests. */
+FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion version);
+
+/** A convenience method for creating docs for tests. */
+FSTDocument *FSTTestDoc(NSString *path,
+ FSTTestSnapshotVersion version,
+ NSDictionary<NSString *, id> *data,
+ BOOL hasMutations);
+
+/** A convenience method for creating deleted docs for tests. */
+FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version);
+
+/** A convenience method for creating resource paths from a path string. */
+FSTResourcePath *FSTTestPath(NSString *path);
+
+/**
+ * A convenience method for creating a document reference from a path string.
+ */
+FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *databaseID, NSString *path);
+
+/** A convenience method for creating a query for the given path (without any other filters). */
+FSTQuery *FSTTestQuery(NSString *path);
+
+/**
+ * A convenience method to create a FSTFilter using a string representation for both field
+ * and operator (<, <=, ==, >=, >).
+ */
+id<FSTFilter> FSTTestFilter(NSString *field, NSString *op, id value);
+
+/** A convenience method for creating sort orders. */
+FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction);
+
+/**
+ * Creates an NSComparator that will compare FSTDocuments by the given fieldPath string then by
+ * key.
+ */
+NSComparator FSTTestDocComparator(NSString *fieldPath);
+
+/**
+ * Creates a FSTDocumentSet based on the given comparator, initially containing the given
+ * documents.
+ */
+FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray<FSTDocument *> *docs);
+
+/** Computes changes to the view with the docs and then applies them and returns the snapshot. */
+FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view,
+ NSArray<FSTMaybeDocument *> *docs,
+ FSTTargetChange *_Nullable targetChange);
+
+/** Creates a set mutation for the document key at the given path. */
+FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary<NSString *, id> *values);
+
+/** Creates a patch mutation for the document key at the given path. */
+FSTPatchMutation *FSTTestPatchMutation(NSString *path,
+ NSDictionary<NSString *, id> *values,
+ NSArray<FSTFieldPath *> *_Nullable updateMask);
+
+FSTTransformMutation *FSTTestTransformMutation(NSString *path,
+ NSArray<NSString *> *serverTimestampFields);
+
+/** Creates a delete mutation for the document key at the given path. */
+FSTDeleteMutation *FSTTestDeleteMutation(NSString *path);
+
+/** Converts a list of documents to a sorted map. */
+FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs);
+
+/** Creates a remote event with changes to a document. */
+FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
+ NSArray<NSNumber *> *updatedInTargets,
+ NSArray<NSNumber *> *removedFromTargets);
+
+/** Creates a test view changes. */
+FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query,
+ NSArray<NSString *> *addedKeys,
+ NSArray<NSString *> *removedKeys);
+
+/** Creates a resume token to match the given snapshot version. */
+NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot);
+
+#if __cplusplus
+} // extern "C"
+#endif
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.m b/Firestore/Example/Tests/Util/FSTHelpers.m
new file mode 100644
index 0000000..3b7f47f
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTHelpers.m
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTHelpers.h"
+
+#import "API/FIRFieldPath+Internal.h"
+#import "API/FSTUserDataConverter.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTTimestamp.h"
+#import "Core/FSTView.h"
+#import "Firestore/FIRFieldPath.h"
+#import "Firestore/FIRGeoPoint.h"
+#import "Local/FSTLocalViewChanges.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTDocumentSet.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTRemoteEvent.h"
+#import "Remote/FSTWatchChange.h"
+#import "Util/FSTAssert.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** A string sentinel that can be used with FSTTestPatchMutation() to mark a field for deletion. */
+static NSString *const kDeleteSentinel = @"<DELETE>";
+
+static const int kMicrosPerSec = 1000000;
+static const int kMillisPerSec = 1000;
+
+FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second) {
+ NSDate *date = FSTTestDate(year, month, day, hour, minute, second);
+ return [FSTTimestamp timestampWithDate:date];
+}
+
+NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second) {
+ NSDateComponents *comps = FSTTestDateComponents(year, month, day, hour, minute, second);
+ return [[NSCalendar currentCalendar] dateFromComponents:comps];
+}
+
+NSData *FSTTestData(int bytes, ...) {
+ va_list args;
+ va_start(args, bytes); /* Initialize the argument list. */
+
+ NSMutableData *data = [NSMutableData data];
+
+ int next = bytes;
+ while (next >= 0) {
+ uint8_t byte = (uint8_t)next;
+ [data appendBytes:&byte length:1];
+ next = va_arg(args, int);
+ }
+
+ va_end(args);
+ return [data copy];
+}
+
+FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude) {
+ return [[FIRGeoPoint alloc] initWithLatitude:latitude longitude:longitude];
+}
+
+NSDateComponents *FSTTestDateComponents(
+ int year, int month, int day, int hour, int minute, int second) {
+ NSDateComponents *comps = [[NSDateComponents alloc] init];
+ comps.year = year;
+ comps.month = month;
+ comps.day = day;
+ comps.hour = hour;
+ comps.minute = minute;
+ comps.second = second;
+
+ // Force time zone to UTC to avoid these values changing due to daylight saving.
+ comps.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
+ return comps;
+}
+
+FSTFieldPath *FSTTestFieldPath(NSString *field) {
+ return [FIRFieldPath pathWithDotSeparatedString:field].internalValue;
+}
+
+FSTFieldValue *FSTTestFieldValue(id _Nullable value) {
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID];
+ FSTUserDataConverter *converter =
+ [[FSTUserDataConverter alloc] initWithDatabaseID:databaseID
+ preConverter:^id _Nullable(id _Nullable input) {
+ return input;
+ }];
+ // HACK: We use parsedQueryValue: since it accepts scalars as well as arrays / objects, and
+ // our tests currently use FSTTestFieldValue() pretty generically so we don't know the intent.
+ return [converter parsedQueryValue:value];
+}
+
+FSTObjectValue *FSTTestObjectValue(NSDictionary<NSString *, id> *data) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(data);
+ FSTCAssert([wrapped isKindOfClass:[FSTObjectValue class]], @"Unsupported value: %@", data);
+ return (FSTObjectValue *)wrapped;
+}
+
+FSTDocumentKey *FSTTestDocKey(NSString *path) {
+ return [FSTDocumentKey keyWithPathString:path];
+}
+
+FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys) {
+ FSTDocumentKeySet *result = [FSTDocumentKeySet keySet];
+ for (FSTDocumentKey *key in keys) {
+ result = [result setByAddingObject:key];
+ }
+ return result;
+}
+
+FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion versionMicroseconds) {
+ int64_t seconds = versionMicroseconds / kMicrosPerSec;
+ int32_t nanos = (int32_t)(versionMicroseconds % kMicrosPerSec) * kMillisPerSec;
+
+ FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos];
+ return [FSTSnapshotVersion versionWithTimestamp:timestamp];
+}
+
+FSTDocument *FSTTestDoc(NSString *path,
+ FSTTestSnapshotVersion version,
+ NSDictionary<NSString *, id> *data,
+ BOOL hasMutations) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path];
+ return [FSTDocument documentWithData:FSTTestObjectValue(data)
+ key:key
+ version:FSTTestVersion(version)
+ hasLocalMutations:hasMutations];
+}
+
+FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path];
+ return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)];
+}
+
+static NSArray<NSString *> *FSTTestSplitPath(NSString *path) {
+ if ([path isEqualToString:@""]) {
+ return @[];
+ } else {
+ return [path componentsSeparatedByString:@"/"];
+ }
+}
+
+FSTResourcePath *FSTTestPath(NSString *path) {
+ return [FSTResourcePath pathWithSegments:FSTTestSplitPath(path)];
+}
+
+FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *database, NSString *path) {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database];
+ return [[FSTDocumentKeyReference alloc] initWithKey:FSTTestDocKey(path) databaseID:databaseID];
+}
+
+FSTQuery *FSTTestQuery(NSString *path) {
+ return [FSTQuery queryWithPath:FSTTestPath(path)];
+}
+
+id<FSTFilter> FSTTestFilter(NSString *field, NSString *opString, id value) {
+ FSTFieldPath *path = FSTTestFieldPath(field);
+ FSTRelationFilterOperator op;
+ if ([opString isEqualToString:@"<"]) {
+ op = FSTRelationFilterOperatorLessThan;
+ } else if ([opString isEqualToString:@"<="]) {
+ op = FSTRelationFilterOperatorLessThanOrEqual;
+ } else if ([opString isEqualToString:@"=="]) {
+ op = FSTRelationFilterOperatorEqual;
+ } else if ([opString isEqualToString:@">="]) {
+ op = FSTRelationFilterOperatorGreaterThanOrEqual;
+ } else if ([opString isEqualToString:@">"]) {
+ op = FSTRelationFilterOperatorGreaterThan;
+ } else {
+ FSTCFail(@"Unsupported operator type: %@", opString);
+ }
+
+ FSTFieldValue *data = FSTTestFieldValue(value);
+ if ([data isEqual:[FSTDoubleValue nanValue]]) {
+ FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with NAN.");
+ return [[FSTNanFilter alloc] initWithField:path];
+ } else if ([data isEqual:[FSTNullValue nullValue]]) {
+ FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with Null.");
+ return [[FSTNullFilter alloc] initWithField:path];
+ } else {
+ return [FSTRelationFilter filterWithField:path filterOperator:op value:data];
+ }
+}
+
+FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction) {
+ FSTFieldPath *path = FSTTestFieldPath(field);
+ BOOL ascending;
+ if ([direction isEqualToString:@"asc"]) {
+ ascending = YES;
+ } else if ([direction isEqualToString:@"desc"]) {
+ ascending = NO;
+ } else {
+ FSTCFail(@"Unsupported direction: %@", direction);
+ }
+ return [FSTSortOrder sortOrderWithFieldPath:path ascending:ascending];
+}
+
+NSComparator FSTTestDocComparator(NSString *fieldPath) {
+ FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"docs" ]]]
+ queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(fieldPath)
+ ascending:YES]];
+ return [query comparator];
+}
+
+FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray<FSTDocument *> *docs) {
+ FSTDocumentSet *docSet = [FSTDocumentSet documentSetWithComparator:comp];
+ for (FSTDocument *doc in docs) {
+ docSet = [docSet documentSetByAddingDocument:doc];
+ }
+ return docSet;
+}
+
+FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary<NSString *, id> *values) {
+ return [[FSTSetMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path]
+ value:FSTTestObjectValue(values)
+ precondition:[FSTPrecondition none]];
+}
+
+FSTPatchMutation *FSTTestPatchMutation(NSString *path,
+ NSDictionary<NSString *, id> *values,
+ NSArray<FSTFieldPath *> *_Nullable updateMask) {
+ BOOL merge = updateMask != nil;
+
+ __block FSTObjectValue *objectValue = [FSTObjectValue objectValue];
+ NSMutableArray<FSTFieldPath *> *fieldMaskPaths = [NSMutableArray array];
+ [values enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+ FSTFieldPath *path = FSTTestFieldPath(key);
+ [fieldMaskPaths addObject:path];
+ if (![value isEqual:kDeleteSentinel]) {
+ FSTFieldValue *parsedValue = FSTTestFieldValue(value);
+ objectValue = [objectValue objectBySettingValue:parsedValue forPath:path];
+ }
+ }];
+
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)];
+ FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:merge ? updateMask : fieldMaskPaths];
+ return [[FSTPatchMutation alloc] initWithKey:key
+ fieldMask:mask
+ value:objectValue
+ precondition:[FSTPrecondition preconditionWithExists:YES]];
+}
+
+// For now this only creates TransformMutations with server timestamps.
+FSTTransformMutation *FSTTestTransformMutation(NSString *path,
+ NSArray<NSString *> *serverTimestampFields) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)];
+ NSMutableArray<FSTFieldTransform *> *fieldTransforms = [NSMutableArray array];
+ for (NSString *field in serverTimestampFields) {
+ FSTFieldPath *fieldPath = FSTTestFieldPath(field);
+ id<FSTTransformOperation> transformOp = [FSTServerTimestampTransform serverTimestampTransform];
+ FSTFieldTransform *transform =
+ [[FSTFieldTransform alloc] initWithPath:fieldPath transform:transformOp];
+ [fieldTransforms addObject:transform];
+ }
+ return [[FSTTransformMutation alloc] initWithKey:key fieldTransforms:fieldTransforms];
+}
+
+FSTDeleteMutation *FSTTestDeleteMutation(NSString *path) {
+ return [[FSTDeleteMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path]
+ precondition:[FSTPrecondition none]];
+}
+
+FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs) {
+ FSTMaybeDocumentDictionary *updates = [FSTMaybeDocumentDictionary maybeDocumentDictionary];
+ for (FSTMaybeDocument *doc in docs) {
+ updates = [updates dictionaryBySettingObject:doc forKey:doc.key];
+ }
+ return updates;
+}
+
+FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view,
+ NSArray<FSTMaybeDocument *> *docs,
+ FSTTargetChange *_Nullable targetChange) {
+ return [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)]
+ targetChange:targetChange]
+ .snapshot;
+}
+
+FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
+ NSArray<NSNumber *> *updatedInTargets,
+ NSArray<NSNumber *> *removedFromTargets) {
+ FSTDocumentWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets
+ removedTargetIDs:removedFromTargets
+ documentKey:doc.key
+ document:doc];
+ NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
+ FSTQueryData *dummyQueryData = [FSTQueryData alloc];
+ for (NSNumber *targetID in updatedInTargets) {
+ listens[targetID] = dummyQueryData;
+ }
+ for (NSNumber *targetID in removedFromTargets) {
+ listens[targetID] = dummyQueryData;
+ }
+ NSMutableDictionary<NSNumber *, NSNumber *> *pending = [NSMutableDictionary dictionary];
+ FSTWatchChangeAggregator *aggregator =
+ [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version
+ listenTargets:listens
+ pendingTargetResponses:pending];
+ [aggregator addWatchChange:change];
+ return [aggregator remoteEvent];
+}
+
+/** Creates a resume token to match the given snapshot version. */
+NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion snapshotVersion) {
+ if (snapshotVersion == 0) {
+ return nil;
+ }
+
+ NSString *snapshotString = [NSString stringWithFormat:@"snapshot-%" PRId64, snapshotVersion];
+ return [snapshotString dataUsingEncoding:NSUTF8StringEncoding];
+}
+
+FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query,
+ NSArray<NSString *> *addedKeys,
+ NSArray<NSString *> *removedKeys) {
+ FSTDocumentKeySet *added = [FSTDocumentKeySet keySet];
+ for (NSString *keyPath in addedKeys) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath];
+ added = [added setByAddingObject:key];
+ }
+ FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet];
+ for (NSString *keyPath in removedKeys) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath];
+ removed = [removed setByAddingObject:key];
+ }
+ return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed];
+}
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h
new file mode 100644
index 0000000..cefd669
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h
@@ -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 <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import "XCTestCase+Await.h"
+
+@class FIRCollectionReference;
+@class FIRDocumentSnapshot;
+@class FIRDocumentReference;
+@class FIRQuerySnapshot;
+@class FIRFirestore;
+@class FIRFirestoreSettings;
+@class FIRQuery;
+@class FSTEventAccumulator;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTIntegrationTestCase : XCTestCase
+
+/** Returns the default Firestore project ID for testing. */
++ (NSString *)projectID;
+
+/** Returns a FirestoreSettings configured to use either hexa or the emulator. */
++ (FIRFirestoreSettings *)settings;
+
+/** Returns a new Firestore connected to the "test-db" project. */
+- (FIRFirestore *)firestore;
+
+/** Returns a new Firestore connected to the project with the given projectID. */
+- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID;
+
+/** Synchronously shuts down the given firestore. */
+- (void)shutdownFirestore:(FIRFirestore *)firestore;
+
+- (NSString *)documentPath;
+
+- (FIRDocumentReference *)documentRef;
+
+- (FIRCollectionReference *)collectionRef;
+
+- (FIRCollectionReference *)collectionRefWithDocuments:
+ (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents;
+
+- (void)writeAllDocuments:(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents
+ toCollection:(FIRCollectionReference *)collection;
+
+- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path,
+ FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef))action;
+
+- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref;
+
+- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query;
+
+- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data;
+
+- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data;
+
+- (void)deleteDocumentRef:(FIRDocumentReference *)ref;
+
+/**
+ * "Blocks" the current thread/run loop until the block returns YES.
+ * Should only be called on the main thread.
+ * The block is invoked frequently and in a loop (every couple of millseconds) to ensure fast
+ * test progress and make sure actions to be run on main thread are not blocked by this method.
+ */
+- (void)waitUntil:(BOOL (^)())predicate;
+
+@property(nonatomic, strong) FIRFirestore *db;
+@property(nonatomic, strong) FSTEventAccumulator *eventAccumulator;
+@end
+
+/** Converts the FIRQuerySnapshot to an NSArray containing the data of the documents in order. */
+NSArray<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs);
+
+/** Converts the FIRQuerySnapshot to an NSArray containing the document IDs in order. */
+NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m
new file mode 100644
index 0000000..87a78c3
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import "FSTIntegrationTestCase.h"
+
+#import <FirebaseCommunity/FIRLogger.h>
+#import <GRPCClient/GRPCCall+ChannelArg.h>
+#import <GRPCClient/GRPCCall+Tests.h>
+
+#import "API/FIRFirestore+Internal.h"
+#import "Auth/FSTEmptyCredentialsProvider.h"
+#import "Local/FSTLevelDB.h"
+#import "Model/FSTDatabaseID.h"
+#import "Util/FSTDispatchQueue.h"
+#import "Util/FSTUtil.h"
+
+#import "FSTEventAccumulator.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTIntegrationTestCase {
+ NSMutableArray<FIRFirestore *> *_firestores;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ [self clearPersistence];
+
+ _firestores = [NSMutableArray array];
+ self.db = [self firestore];
+ self.eventAccumulator = [FSTEventAccumulator accumulatorForTest:self];
+}
+
+- (void)tearDown {
+ @try {
+ for (FIRFirestore *firestore in _firestores) {
+ [self shutdownFirestore:firestore];
+ }
+ } @finally {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [GRPCCall closeOpenConnections];
+#pragma clang diagnostic pop
+ _firestores = nil;
+ [super tearDown];
+ }
+}
+
+- (void)clearPersistence {
+ NSString *levelDBDir = [FSTLevelDB documentsDirectory];
+ NSError *error;
+ if (![[NSFileManager defaultManager] removeItemAtPath:levelDBDir error:&error]) {
+ // file not found is okay.
+ XCTAssertTrue(
+ [error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError,
+ @"Failed to clear LevelDB Persistence: %@", error);
+ }
+}
+
+- (FIRFirestore *)firestore {
+ return [self firestoreWithProjectID:[FSTIntegrationTestCase projectID]];
+}
+
++ (NSString *)projectID {
+ NSString *project = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"];
+ if (!project) {
+ project = @"test-db";
+ }
+ return project;
+}
+
++ (FIRFirestoreSettings *)settings {
+ FIRFirestoreSettings *settings = [[FIRFirestoreSettings alloc] init];
+ NSString *host = [[NSProcessInfo processInfo] environment][@"DATASTORE_HOST"];
+ settings.sslEnabled = YES;
+ if (!host) {
+ // If host is nil, there is no GoogleService-Info.plist. Check if a hexa integration test
+ // configuration is configured. The first bundle location is used by bazel builds. The
+ // second is used for github clones.
+ host = @"localhost:8081";
+ settings.sslEnabled = YES;
+ NSString *certsPath =
+ [[NSBundle mainBundle] pathForResource:@"PlugIns/IntegrationTests.xctest/CAcert"
+ ofType:@"pem"];
+ if (certsPath == nil) {
+ certsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"CAcert" ofType:@"pem"];
+ }
+ unsigned long long fileSize =
+ [[[NSFileManager defaultManager] attributesOfItemAtPath:certsPath error:nil] fileSize];
+
+ if (fileSize == 0) {
+ NSLog(
+ @"The cert is not properly configured. Make sure setup_integration_tests.py "
+ "has been run.");
+ }
+ [GRPCCall useTestCertsPath:certsPath testName:@"test_cert_2" forHost:host];
+ }
+ settings.host = host;
+ settings.persistenceEnabled = YES;
+ NSLog(@"Configured integration test for %@ with SSL: %@", settings.host,
+ settings.sslEnabled ? @"YES" : @"NO");
+ return settings;
+}
+
+- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID {
+ NSString *persistenceKey = [NSString stringWithFormat:@"db%lu", (unsigned long)_firestores.count];
+
+ FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue
+ queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)];
+
+ FSTEmptyCredentialsProvider *credentialsProvider = [[FSTEmptyCredentialsProvider alloc] init];
+
+ FIRSetLoggerLevel(FIRLoggerLevelDebug);
+ // HACK: FIRFirestore expects a non-nil app, but for tests we cheat.
+ FIRApp *app = nil;
+ FIRFirestore *firestore = [[FIRFirestore alloc] initWithProjectID:projectID
+ database:kDefaultDatabaseID
+ persistenceKey:persistenceKey
+ credentialsProvider:credentialsProvider
+ workerDispatchQueue:workerDispatchQueue
+ firebaseApp:app];
+
+ firestore.settings = [FSTIntegrationTestCase settings];
+
+ [_firestores addObject:firestore];
+ return firestore;
+}
+
+- (void)shutdownFirestore:(FIRFirestore *)firestore {
+ XCTestExpectation *shutdownCompletion = [self expectationWithDescription:@"shutdown"];
+ [firestore shutdownWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [shutdownCompletion fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (NSString *)documentPath {
+ return [@"test-collection/" stringByAppendingString:[FSTUtil autoID]];
+}
+
+- (FIRDocumentReference *)documentRef {
+ return [self.db documentWithPath:[self documentPath]];
+}
+
+- (FIRCollectionReference *)collectionRef {
+ NSString *collectionName = [@"test-collection-" stringByAppendingString:[FSTUtil autoID]];
+ return [self.db collectionWithPath:collectionName];
+}
+
+- (FIRCollectionReference *)collectionRefWithDocuments:
+ (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents {
+ FIRCollectionReference *collection = [self collectionRef];
+ // Use a different instance to write the documents
+ [self writeAllDocuments:documents
+ toCollection:[[self firestore] collectionWithPath:collection.path]];
+ return collection;
+}
+
+- (void)writeAllDocuments:(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents
+ toCollection:(FIRCollectionReference *)collection {
+ [documents enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary<NSString *, id> *value,
+ BOOL *stop) {
+ FIRDocumentReference *ref = [collection documentWithPath:key];
+ [self writeDocumentRef:ref data:value];
+ }];
+}
+
+- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path,
+ FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef))action {
+ FIRFirestore *reader = self.db; // for clarity
+ FIRFirestore *writer = [self firestore];
+
+ NSString *path = [self documentPath];
+ FIRDocumentReference *readerRef = [reader documentWithPath:path];
+ FIRDocumentReference *writerRef = [writer documentWithPath:path];
+ action(path, readerRef, writerRef);
+}
+
+- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref {
+ __block FIRDocumentSnapshot *result;
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"getData"];
+ [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *doc, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ result = doc;
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ return result;
+}
+
+- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query {
+ __block FIRQuerySnapshot *result;
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"getData"];
+ [query getDocumentsWithCompletion:^(FIRQuerySnapshot *documentSet, NSError *error) {
+ XCTAssertNil(error);
+ result = documentSet;
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ return result;
+}
+
+- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"setData"];
+ [ref setData:data
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<id, id> *)data {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"updateData"];
+ [ref updateData:data
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)deleteDocumentRef:(FIRDocumentReference *)ref {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"deleteDocument"];
+ [ref deleteDocumentWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)waitUntil:(BOOL (^)())predicate {
+ NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
+ double waitSeconds = [self defaultExpectationWaitSeconds];
+ while (!predicate() && ([NSDate timeIntervalSinceReferenceDate] - start < waitSeconds)) {
+ // This waits for the next event or until the 100ms timeout is reached
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
+ beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
+ }
+ if (!predicate()) {
+ XCTFail(@"Timeout");
+ }
+}
+
+NSArray<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs) {
+ NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
+ for (FIRDocumentSnapshot *doc in docs.documents) {
+ [result addObject:doc.data];
+ }
+ return result;
+}
+
+NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs) {
+ NSMutableArray<NSString *> *result = [NSMutableArray array];
+ for (FIRDocumentSnapshot *doc in docs.documents) {
+ [result addObject:doc.documentID];
+ }
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTUtilTests.m b/Firestore/Example/Tests/Util/FSTUtilTests.m
new file mode 100644
index 0000000..998832d
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTUtilTests.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 "Util/FSTUtil.h"
+
+#import <XCTest/XCTest.h>
+
+@interface FSTUtilTests : XCTestCase
+@end
+
+@implementation FSTUtilTests
+
+- (void)testAutoID {
+ NSString *autoID = [FSTUtil autoID];
+ XCTAssertEqual([autoID length], 20);
+ for (NSUInteger i = 0; i < 20; i++) {
+ unichar c = [autoID characterAtIndex:i];
+ XCTAssert(c >= ' ' && c <= '~', @"Should be printable ascii characters.");
+ }
+}
+
+@end
diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.h b/Firestore/Example/Tests/Util/XCTestCase+Await.h
new file mode 100644
index 0000000..9d575f9
--- /dev/null
+++ b/Firestore/Example/Tests/Util/XCTestCase+Await.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 <XCTest/XCTest.h>
+
+@interface XCTestCase (Await)
+
+/**
+ * Await all outstanding expectations with a reasonable timeout, and if any of them fail, XCTFail
+ * the test.
+ */
+- (void)awaitExpectations;
+
+/**
+ * Returns a reasonable timeout for testing against Firestore.
+ */
+- (double)defaultExpectationWaitSeconds;
+
+@end
diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.m b/Firestore/Example/Tests/Util/XCTestCase+Await.m
new file mode 100644
index 0000000..e200c8c
--- /dev/null
+++ b/Firestore/Example/Tests/Util/XCTestCase+Await.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 "XCTestCase+Await.h"
+
+#import <Foundation/Foundation.h>
+
+static const double kExpectationWaitSeconds = 10.0;
+
+@implementation XCTestCase (Await)
+
+- (void)awaitExpectations {
+ [self waitForExpectationsWithTimeout:kExpectationWaitSeconds
+ handler:^(NSError *_Nullable expectationError) {
+ if (expectationError) {
+ XCTFail(@"Error waiting for timeout: %@", expectationError);
+ }
+ }];
+}
+
+- (double)defaultExpectationWaitSeconds {
+ return kExpectationWaitSeconds;
+}
+
+@end
diff --git a/Firestore/Example/Tests/en.lproj/InfoPlist.strings b/Firestore/Example/Tests/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/Firestore/Example/Tests/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/Firestore/Firestore.podspec b/Firestore/Firestore.podspec
new file mode 100644
index 0000000..efd3f64
--- /dev/null
+++ b/Firestore/Firestore.podspec
@@ -0,0 +1,44 @@
+#
+# Be sure to run `pod lib lint Firestore.podspec' to ensure this is a
+# valid spec before submitting.
+#
+# Any lines starting with a # are optional, but their use is encouraged
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
+#
+
+Pod::Spec.new do |s|
+ s.name = 'Firestore'
+ s.version = '0.1.0'
+ s.summary = 'Google Cloud Firestore for iOS'
+
+ s.description = <<-DESC
+Google Cloud Firestore is a NoSQL document database built for automatic scaling, high performance, and ease of application development.
+ DESC
+
+ s.homepage = 'https://developers.google.com/'
+ s.license = { :type => 'Apache', :file => '../LICENSE' }
+ s.authors = 'Google, Inc.'
+
+ s.source = { :git => 'https://github.com/TBD/Firestore.git', :tag => s.version.to_s }
+ # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
+
+ s.ios.deployment_target = '8.0'
+
+ s.source_files = 'Source/**/*', 'Port/**/*', 'Protos/objc/**/*.[hm]', 'third_party/**/*.[mh]'
+ s.requires_arc = 'Source/**/*', 'third_party/**/*.[mh]'
+ s.exclude_files = 'Port/*test.cc', 'third_party/**/Tests/**'
+ s.public_header_files = 'Source/Public/*.h'
+ s.frameworks = 'MobileCoreServices'
+ s.dependency 'gRPC-ProtoRPC'
+ s.dependency 'leveldb-library'
+ s.dependency 'Protobuf'
+ s.dependency 'FirebaseCommunity/Core'
+ s.dependency 'FirebaseCommunity/Auth'
+ s.library = 'c++'
+
+ s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+ '$(inherited) ' +
+ 'GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1 ',
+ 'OTHER_CFLAGS' => '-DFIRFirestore_VERSION=' + s.version.to_s
+ }
+end
diff --git a/Firestore/Port/absl/absl_attributes.h b/Firestore/Port/absl/absl_attributes.h
new file mode 100644
index 0000000..d43930c
--- /dev/null
+++ b/Firestore/Port/absl/absl_attributes.h
@@ -0,0 +1,644 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Various macros for C++ attributes
+// Most macros here are exposing GCC or Clang features, and are stubbed out for
+// other compilers.
+// GCC attributes documentation:
+// https://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Function-Attributes.html
+// https://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Variable-Attributes.html
+// https://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Type-Attributes.html
+//
+// Most attributes in this file are already supported by GCC 4.7.
+// However, some of them are not supported in older version of Clang.
+// Thus, we check __has_attribute() first. If the check fails, we check if we
+// are on GCC and assume the attribute exists on GCC (which is verified on GCC
+// 4.7).
+//
+// For sanitizer-related attributes, define the following macros
+// using -D along with the given value for -fsanitize:
+// - ADDRESS_SANITIZER with -fsanitize=address (GCC 4.8+, Clang)
+// - MEMORY_SANITIZER with -fsanitize=memory (Clang)
+// - THREAD_SANITIZER with -fsanitize=thread (GCC 4.8+, Clang)
+// - UNDEFINED_BEHAVIOR_SANITIZER with -fsanitize=undefined (GCC 4.9+, Clang)
+// - CONTROL_FLOW_INTEGRITY with -fsanitize=cfi (Clang)
+// Since these are only supported by GCC and Clang now, we only check for
+// __GNUC__ (GCC or Clang) and the above macros.
+#ifndef THIRD_PARTY_ABSL_BASE_ATTRIBUTES_H_
+#define THIRD_PARTY_ABSL_BASE_ATTRIBUTES_H_
+
+// ABSL_HAVE_ATTRIBUTE is a function-like feature checking macro.
+// It's a wrapper around __has_attribute, which is defined by GCC 5+ and Clang.
+// It evaluates to a nonzero constant integer if the attribute is supported
+// or 0 if not.
+// It evaluates to zero if __has_attribute is not defined by the compiler.
+// GCC: https://gcc.gnu.org/gcc-5/changes.html
+// Clang: https://clang.llvm.org/docs/LanguageExtensions.html
+#ifdef __has_attribute
+#define ABSL_HAVE_ATTRIBUTE(x) __has_attribute(x)
+#else
+#define ABSL_HAVE_ATTRIBUTE(x) 0
+#endif
+
+// ABSL_HAVE_CPP_ATTRIBUTE is a function-like feature checking macro that
+// accepts C++11 style attributes. It's a wrapper around __has_cpp_attribute,
+// defined by ISO C++ SD-6
+// (http://en.cppreference.com/w/cpp/experimental/feature_test). If we don't
+// find __has_cpp_attribute, will evaluate to 0.
+#if defined(__cplusplus) && defined(__has_cpp_attribute)
+// NOTE: requiring __cplusplus above should not be necessary, but
+// works around https://bugs.llvm.org/show_bug.cgi?id=23435.
+#define ABSL_HAVE_CPP_ATTRIBUTE(x) __has_cpp_attribute(x)
+#else
+#define ABSL_HAVE_CPP_ATTRIBUTE(x) 0
+#endif
+
+// -----------------------------------------------------------------------------
+// Function Attributes
+// -----------------------------------------------------------------------------
+// GCC: https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html
+// Clang: https://clang.llvm.org/docs/AttributeReference.html
+
+// PRINTF_ATTRIBUTE, SCANF_ATTRIBUTE
+// Tell the compiler to do printf format std::string checking if the
+// compiler supports it; see the 'format' attribute in
+// <http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Function-Attributes.html>.
+//
+// N.B.: As the GCC manual states, "[s]ince non-static C++ methods
+// have an implicit 'this' argument, the arguments of such methods
+// should be counted from two, not one."
+#if ABSL_HAVE_ATTRIBUTE(format) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_PRINTF_ATTRIBUTE(string_index, first_to_check) \
+ __attribute__((__format__(__printf__, string_index, first_to_check)))
+#define ABSL_SCANF_ATTRIBUTE(string_index, first_to_check) \
+ __attribute__((__format__(__scanf__, string_index, first_to_check)))
+#else
+#define ABSL_PRINTF_ATTRIBUTE(string_index, first_to_check)
+#define ABSL_SCANF_ATTRIBUTE(string_index, first_to_check)
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(format) || (defined(__GNUC__) && !defined(__clang__))
+#define PRINTF_ATTRIBUTE(string_index, first_to_check) \
+ __attribute__((__format__(__printf__, string_index, first_to_check)))
+#define SCANF_ATTRIBUTE(string_index, first_to_check) \
+ __attribute__((__format__(__scanf__, string_index, first_to_check)))
+#else
+#define PRINTF_ATTRIBUTE(string_index, first_to_check)
+#define SCANF_ATTRIBUTE(string_index, first_to_check)
+#endif
+
+// ATTRIBUTE_ALWAYS_INLINE, ATTRIBUTE_NOINLINE
+// For functions we want to force inline or not inline.
+// Introduced in gcc 3.1.
+#if ABSL_HAVE_ATTRIBUTE(always_inline) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_ATTRIBUTE_ALWAYS_INLINE __attribute__((always_inline))
+#define ABSL_HAVE_ATTRIBUTE_ALWAYS_INLINE 1
+#else
+#define ABSL_ATTRIBUTE_ALWAYS_INLINE
+#endif
+
+#if ABSL_HAVE_ATTRIBUTE(noinline) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_ATTRIBUTE_NOINLINE __attribute__((noinline))
+#define ABSL_HAVE_ATTRIBUTE_NOINLINE 1
+#else
+#define ABSL_ATTRIBUTE_NOINLINE
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(always_inline) || (defined(__GNUC__) && !defined(__clang__))
+#define ATTRIBUTE_ALWAYS_INLINE __attribute__((always_inline))
+#define HAVE_ATTRIBUTE_ALWAYS_INLINE 1
+#else
+#define ATTRIBUTE_ALWAYS_INLINE
+#endif
+
+#if ABSL_HAVE_ATTRIBUTE(noinline) || (defined(__GNUC__) && !defined(__clang__))
+#define ATTRIBUTE_NOINLINE __attribute__((noinline))
+#define HAVE_ATTRIBUTE_NOINLINE 1
+#else
+#define ATTRIBUTE_NOINLINE
+#endif
+
+// ATTRIBUTE_NO_TAIL_CALL
+// Prevent the compiler from optimizing away stack frames for functions which
+// end in a call to another function.
+#if ABSL_HAVE_ATTRIBUTE(disable_tail_calls)
+#define ABSL_HAVE_ATTRIBUTE_NO_TAIL_CALL 1
+#define ABSL_ATTRIBUTE_NO_TAIL_CALL __attribute__((disable_tail_calls))
+#elif defined(__GNUC__) && !defined(__clang__)
+#define ABSL_HAVE_ATTRIBUTE_NO_TAIL_CALL 1
+#define ABSL_ATTRIBUTE_NO_TAIL_CALL __attribute__((optimize("no-optimize-sibling-calls")))
+#else
+#define ABSL_ATTRIBUTE_NO_TAIL_CALL
+#define ABSL_HAVE_ATTRIBUTE_NO_TAIL_CALL 0
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(disable_tail_calls)
+#define HAVE_ATTRIBUTE_NO_TAIL_CALL 1
+#define ATTRIBUTE_NO_TAIL_CALL __attribute__((disable_tail_calls))
+#elif defined(__GNUC__) && !defined(__clang__)
+#define HAVE_ATTRIBUTE_NO_TAIL_CALL 1
+#define ATTRIBUTE_NO_TAIL_CALL __attribute__((optimize("no-optimize-sibling-calls")))
+#else
+#define ATTRIBUTE_NO_TAIL_CALL
+#define HAVE_ATTRIBUTE_NO_TAIL_CALL 0
+#endif
+
+// ATTRIBUTE_WEAK
+// For weak functions
+#if ABSL_HAVE_ATTRIBUTE(weak) || (defined(__GNUC__) && !defined(__clang__))
+#undef ABSL_ATTRIBUTE_WEAK
+#define ABSL_ATTRIBUTE_WEAK __attribute__((weak))
+#define ABSL_HAVE_ATTRIBUTE_WEAK 1
+#else
+#define ABSL_ATTRIBUTE_WEAK
+#define ABSL_HAVE_ATTRIBUTE_WEAK 0
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(weak) || (defined(__GNUC__) && !defined(__clang__))
+#undef ATTRIBUTE_WEAK
+#define ATTRIBUTE_WEAK __attribute__((weak))
+#define HAVE_ATTRIBUTE_WEAK 1
+#else
+#define ATTRIBUTE_WEAK
+#define HAVE_ATTRIBUTE_WEAK 0
+#endif
+
+// ATTRIBUTE_NONNULL
+// Tell the compiler either that a particular function parameter
+// should be a non-null pointer, or that all pointer arguments should
+// be non-null.
+//
+// Note: As the GCC manual states, "[s]ince non-static C++ methods
+// have an implicit 'this' argument, the arguments of such methods
+// should be counted from two, not one."
+//
+// Args are indexed starting at 1.
+// For non-static class member functions, the implicit "this" argument
+// is arg 1, and the first explicit argument is arg 2.
+// For static class member functions, there is no implicit "this", and
+// the first explicit argument is arg 1.
+//
+// /* arg_a cannot be null, but arg_b can */
+// void Function(void* arg_a, void* arg_b) ATTRIBUTE_NONNULL(1);
+//
+// class C {
+// /* arg_a cannot be null, but arg_b can */
+// void Method(void* arg_a, void* arg_b) ATTRIBUTE_NONNULL(2);
+//
+// /* arg_a cannot be null, but arg_b can */
+// static void StaticMethod(void* arg_a, void* arg_b) ATTRIBUTE_NONNULL(1);
+// };
+//
+// If no arguments are provided, then all pointer arguments should be non-null.
+//
+// /* No pointer arguments may be null. */
+// void Function(void* arg_a, void* arg_b, int arg_c) ATTRIBUTE_NONNULL();
+//
+// NOTE: The GCC nonnull attribute actually accepts a list of arguments, but
+// ATTRIBUTE_NONNULL does not.
+#if ABSL_HAVE_ATTRIBUTE(nonnull) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_ATTRIBUTE_NONNULL(arg_index) __attribute__((nonnull(arg_index)))
+#else
+#define ABSL_ATTRIBUTE_NONNULL(...)
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(nonnull) || (defined(__GNUC__) && !defined(__clang__))
+#define ATTRIBUTE_NONNULL(arg_index) __attribute__((nonnull(arg_index)))
+#else
+#define ATTRIBUTE_NONNULL(...)
+#endif
+
+// ATTRIBUTE_NORETURN
+// Tell the compiler that a given function never returns
+#if ABSL_HAVE_ATTRIBUTE(noreturn) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_ATTRIBUTE_NORETURN __attribute__((noreturn))
+#elif defined(_MSC_VER)
+#define ABSL_ATTRIBUTE_NORETURN __declspec(noreturn)
+#else
+#define ABSL_ATTRIBUTE_NORETURN
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(noreturn) || (defined(__GNUC__) && !defined(__clang__))
+#define ATTRIBUTE_NORETURN __attribute__((noreturn))
+#elif defined(_MSC_VER)
+#define ATTRIBUTE_NORETURN __declspec(noreturn)
+#else
+#define ATTRIBUTE_NORETURN
+#endif
+
+// ATTRIBUTE_NO_SANITIZE_ADDRESS
+// Tell AddressSanitizer (or other memory testing tools) to ignore a given
+// function. Useful for cases when a function reads random locations on stack,
+// calls _exit from a cloned subprocess, deliberately accesses buffer
+// out of bounds or does other scary things with memory.
+// NOTE: GCC supports AddressSanitizer(asan) since 4.8.
+// https://gcc.gnu.org/gcc-4.8/changes.html
+#if defined(__GNUC__) && defined(ADDRESS_SANITIZER)
+#define ABSL_ATTRIBUTE_NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))
+#else
+#define ABSL_ATTRIBUTE_NO_SANITIZE_ADDRESS
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if defined(__GNUC__) && defined(ADDRESS_SANITIZER)
+#define ATTRIBUTE_NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))
+#else
+#define ATTRIBUTE_NO_SANITIZE_ADDRESS
+#endif
+
+// ATTRIBUTE_NO_SANITIZE_MEMORY
+// Tell MemorySanitizer to relax the handling of a given function. All "Use of
+// uninitialized value" warnings from such functions will be suppressed, and all
+// values loaded from memory will be considered fully initialized.
+// This is similar to the ADDRESS_SANITIZER attribute above, but deals with
+// initializedness rather than addressability issues.
+// NOTE: MemorySanitizer(msan) is supported by Clang but not GCC.
+#if defined(__GNUC__) && defined(MEMORY_SANITIZER)
+#define ABSL_ATTRIBUTE_NO_SANITIZE_MEMORY __attribute__((no_sanitize_memory))
+#else
+#define ABSL_ATTRIBUTE_NO_SANITIZE_MEMORY
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if defined(__GNUC__) && defined(MEMORY_SANITIZER)
+#define ATTRIBUTE_NO_SANITIZE_MEMORY __attribute__((no_sanitize_memory))
+#else
+#define ATTRIBUTE_NO_SANITIZE_MEMORY
+#endif
+
+// ATTRIBUTE_NO_SANITIZE_THREAD
+// Tell ThreadSanitizer to not instrument a given function.
+// If you are adding this attribute, please cc dynamic-tools@ on the cl.
+// NOTE: GCC supports ThreadSanitizer(tsan) since 4.8.
+// https://gcc.gnu.org/gcc-4.8/changes.html
+#if defined(__GNUC__) && defined(THREAD_SANITIZER)
+#define ABSL_ATTRIBUTE_NO_SANITIZE_THREAD __attribute__((no_sanitize_thread))
+#else
+#define ABSL_ATTRIBUTE_NO_SANITIZE_THREAD
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if defined(__GNUC__) && defined(THREAD_SANITIZER)
+#define ATTRIBUTE_NO_SANITIZE_THREAD __attribute__((no_sanitize_thread))
+#else
+#define ATTRIBUTE_NO_SANITIZE_THREAD
+#endif
+
+// ATTRIBUTE_NO_SANITIZE_UNDEFINED
+// Tell UndefinedSanitizer to ignore a given function. Useful for cases
+// where certain behavior (eg. devision by zero) is being used intentionally.
+// NOTE: GCC supports UndefinedBehaviorSanitizer(ubsan) since 4.9.
+// https://gcc.gnu.org/gcc-4.9/changes.html
+#if defined(__GNUC__) && defined(UNDEFINED_BEHAVIOR_SANITIZER)
+#define ABSL_ATTRIBUTE_NO_SANITIZE_UNDEFINED __attribute__((no_sanitize("undefined")))
+#else
+#define ABSL_ATTRIBUTE_NO_SANITIZE_UNDEFINED
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if defined(__GNUC__) && defined(UNDEFINED_BEHAVIOR_SANITIZER)
+#define ATTRIBUTE_NO_SANITIZE_UNDEFINED __attribute__((no_sanitize("undefined")))
+#else
+#define ATTRIBUTE_NO_SANITIZE_UNDEFINED
+#endif
+
+// ATTRIBUTE_NO_SANITIZE_CFI
+// Tell ControlFlowIntegrity sanitizer to not instrument a given function.
+#if defined(__GNUC__) && defined(CONTROL_FLOW_INTEGRITY)
+#define ABSL_ATTRIBUTE_NO_SANITIZE_CFI __attribute__((no_sanitize("cfi")))
+#else
+#define ABSL_ATTRIBUTE_NO_SANITIZE_CFI
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if defined(__GNUC__) && defined(CONTROL_FLOW_INTEGRITY)
+#define ATTRIBUTE_NO_SANITIZE_CFI __attribute__((no_sanitize("cfi")))
+#else
+#define ATTRIBUTE_NO_SANITIZE_CFI
+#endif
+
+// ATTRIBUTE_SECTION
+// Labeled sections are not supported on Darwin/iOS.
+#ifdef ABSL_HAVE_ATTRIBUTE_SECTION
+#error ABSL_HAVE_ATTRIBUTE_SECTION cannot be directly set
+#elif (ABSL_HAVE_ATTRIBUTE(section) || (defined(__GNUC__) && !defined(__clang__))) && \
+ !(defined(__APPLE__) && defined(__MACH__))
+#define ABSL_HAVE_ATTRIBUTE_SECTION 1
+//
+// Tell the compiler/linker to put a given function into a section and define
+// "__start_ ## name" and "__stop_ ## name" symbols to bracket the section.
+// This functionality is supported by GNU linker.
+// Any function with ATTRIBUTE_SECTION must not be inlined, or it will
+// be placed into whatever section its caller is placed into.
+//
+#ifndef ABSL_ATTRIBUTE_SECTION
+#define ABSL_ATTRIBUTE_SECTION(name) __attribute__((section(#name))) __attribute__((noinline))
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#ifndef ATTRIBUTE_SECTION
+#define ATTRIBUTE_SECTION(name) __attribute__((section(#name))) __attribute__((noinline))
+#endif
+
+// Tell the compiler/linker to put a given variable into a section and define
+// "__start_ ## name" and "__stop_ ## name" symbols to bracket the section.
+// This functionality is supported by GNU linker.
+#ifndef ABSL_ATTRIBUTE_SECTION_VARIABLE
+#define ABSL_ATTRIBUTE_SECTION_VARIABLE(name) __attribute__((section(#name)))
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#ifndef ATTRIBUTE_SECTION_VARIABLE
+#define ATTRIBUTE_SECTION_VARIABLE(name) __attribute__((section(#name)))
+#endif
+
+//
+// Weak section declaration to be used as a global declaration
+// for ATTRIBUTE_SECTION_START|STOP(name) to compile and link
+// even without functions with ATTRIBUTE_SECTION(name).
+// DEFINE_ATTRIBUTE_SECTION should be in the exactly one file; it's
+// a no-op on ELF but not on Mach-O.
+//
+#ifndef ABSL_DECLARE_ATTRIBUTE_SECTION_VARS
+#define ABSL_DECLARE_ATTRIBUTE_SECTION_VARS(name) \
+ extern char __start_##name[] ATTRIBUTE_WEAK; \
+ extern char __stop_##name[] ATTRIBUTE_WEAK
+#endif
+#ifndef ABSL_DEFINE_ATTRIBUTE_SECTION_VARS
+#define ABSL_INIT_ATTRIBUTE_SECTION_VARS(name)
+#define ABSL_DEFINE_ATTRIBUTE_SECTION_VARS(name)
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#ifndef DECLARE_ATTRIBUTE_SECTION_VARS
+#define DECLARE_ATTRIBUTE_SECTION_VARS(name) \
+ extern char __start_##name[] ATTRIBUTE_WEAK; \
+ extern char __stop_##name[] ATTRIBUTE_WEAK
+#endif
+#ifndef DEFINE_ATTRIBUTE_SECTION_VARS
+#define INIT_ATTRIBUTE_SECTION_VARS(name)
+#define DEFINE_ATTRIBUTE_SECTION_VARS(name)
+#endif
+
+//
+// Return void* pointers to start/end of a section of code with
+// functions having ATTRIBUTE_SECTION(name).
+// Returns 0 if no such functions exits.
+// One must DECLARE_ATTRIBUTE_SECTION_VARS(name) for this to compile and link.
+//
+#define ABSL_ATTRIBUTE_SECTION_START(name) (reinterpret_cast<void *>(__start_##name))
+#define ABSL_ATTRIBUTE_SECTION_STOP(name) (reinterpret_cast<void *>(__stop_##name))
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#define ATTRIBUTE_SECTION_START(name) (reinterpret_cast<void *>(__start_##name))
+#define ATTRIBUTE_SECTION_STOP(name) (reinterpret_cast<void *>(__stop_##name))
+
+#else // !ABSL_HAVE_ATTRIBUTE_SECTION
+
+#define ABSL_HAVE_ATTRIBUTE_SECTION 0
+
+// provide dummy definitions
+#define ABSL_ATTRIBUTE_SECTION(name)
+#define ABSL_ATTRIBUTE_SECTION_VARIABLE(name)
+#define ABSL_INIT_ATTRIBUTE_SECTION_VARS(name)
+#define ABSL_DEFINE_ATTRIBUTE_SECTION_VARS(name)
+#define ABSL_DECLARE_ATTRIBUTE_SECTION_VARS(name)
+#define ABSL_ATTRIBUTE_SECTION_START(name) (reinterpret_cast<void *>(0))
+#define ABSL_ATTRIBUTE_SECTION_STOP(name) (reinterpret_cast<void *>(0))
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#define ATTRIBUTE_SECTION(name)
+#define ATTRIBUTE_SECTION_VARIABLE(name)
+#define INIT_ATTRIBUTE_SECTION_VARS(name)
+#define DEFINE_ATTRIBUTE_SECTION_VARS(name)
+#define DECLARE_ATTRIBUTE_SECTION_VARS(name)
+#define ATTRIBUTE_SECTION_START(name) (reinterpret_cast<void *>(0))
+#define ATTRIBUTE_SECTION_STOP(name) (reinterpret_cast<void *>(0))
+
+#endif // ATTRIBUTE_SECTION
+
+// ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC
+// Support for aligning the stack on 32-bit x86.
+#if ABSL_HAVE_ATTRIBUTE(force_align_arg_pointer) || (defined(__GNUC__) && !defined(__clang__))
+#if defined(__i386__)
+#define ABSL_ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC __attribute__((force_align_arg_pointer))
+#define ABSL_REQUIRE_STACK_ALIGN_TRAMPOLINE (0)
+#elif defined(__x86_64__)
+#define ABSL_REQUIRE_STACK_ALIGN_TRAMPOLINE (1)
+#define ABSL_ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC
+#else // !__i386__ && !__x86_64
+#define ABSL_REQUIRE_STACK_ALIGN_TRAMPOLINE (0)
+#define ABSL_ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC
+#endif // __i386__
+#else
+#define ABSL_ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC
+#define ABSL_REQUIRE_STACK_ALIGN_TRAMPOLINE (0)
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(force_align_arg_pointer) || (defined(__GNUC__) && !defined(__clang__))
+#if defined(__i386__)
+#define ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC __attribute__((force_align_arg_pointer))
+#define REQUIRE_STACK_ALIGN_TRAMPOLINE (0)
+#elif defined(__x86_64__)
+#define REQUIRE_STACK_ALIGN_TRAMPOLINE (1)
+#define ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC
+#else // !__i386__ && !__x86_64
+#define REQUIRE_STACK_ALIGN_TRAMPOLINE (0)
+#define ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC
+#endif // __i386__
+#else
+#define ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC
+#define REQUIRE_STACK_ALIGN_TRAMPOLINE (0)
+#endif
+
+// MUST_USE_RESULT
+// Tell the compiler to warn about unused return values for functions declared
+// with this macro. The macro must appear as the very first part of a function
+// declaration or definition:
+//
+// MUST_USE_RESULT Sprocket* AllocateSprocket();
+//
+// This placement has the broadest compatibility with GCC, Clang, and MSVC, with
+// both defs and decls, and with GCC-style attributes, MSVC declspec, and C++11
+// attributes. Note: past advice was to place the macro after the argument list.
+#if ABSL_HAVE_ATTRIBUTE(warn_unused_result) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_MUST_USE_RESULT __attribute__((warn_unused_result))
+#else
+#define ABSL_MUST_USE_RESULT
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(warn_unused_result) || (defined(__GNUC__) && !defined(__clang__))
+#define MUST_USE_RESULT __attribute__((warn_unused_result))
+#else
+#define MUST_USE_RESULT
+#endif
+
+// ATTRIBUTE_HOT, ATTRIBUTE_COLD
+// Tell GCC that a function is hot or cold. GCC can use this information to
+// improve static analysis, i.e. a conditional branch to a cold function
+// is likely to be not-taken.
+// This annotation is used for function declarations, e.g.:
+// int foo() ATTRIBUTE_HOT;
+#if ABSL_HAVE_ATTRIBUTE(hot) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_ATTRIBUTE_HOT __attribute__((hot))
+#else
+#define ABSL_ATTRIBUTE_HOT
+#endif
+
+#if ABSL_HAVE_ATTRIBUTE(cold) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_ATTRIBUTE_COLD __attribute__((cold))
+#else
+#define ABSL_ATTRIBUTE_COLD
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(hot) || (defined(__GNUC__) && !defined(__clang__))
+#define ATTRIBUTE_HOT __attribute__((hot))
+#else
+#define ATTRIBUTE_HOT
+#endif
+
+#if ABSL_HAVE_ATTRIBUTE(cold) || (defined(__GNUC__) && !defined(__clang__))
+#define ATTRIBUTE_COLD __attribute__((cold))
+#else
+#define ATTRIBUTE_COLD
+#endif
+
+// ABSL_XRAY_ALWAYS_INSTRUMENT, ABSL_XRAY_NEVER_INSTRUMENT, ABSL_XRAY_LOG_ARGS
+//
+// We define the ABSL_XRAY_ALWAYS_INSTRUMENT and ABSL_XRAY_NEVER_INSTRUMENT
+// macro used as an attribute to mark functions that must always or never be
+// instrumented by XRay. Currently, this is only supported in Clang/LLVM.
+//
+// For reference on the LLVM XRay instrumentation, see
+// http://llvm.org/docs/XRay.html.
+//
+// A function with the XRAY_ALWAYS_INSTRUMENT macro attribute in its declaration
+// will always get the XRay instrumentation sleds. These sleds may introduce
+// some binary size and runtime overhead and must be used sparingly.
+//
+// These attributes only take effect when the following conditions are met:
+//
+// - The file/target is built in at least C++11 mode, with a Clang compiler
+// that supports XRay attributes.
+// - The file/target is built with the -fxray-instrument flag set for the
+// Clang/LLVM compiler.
+// - The function is defined in the translation unit (the compiler honors the
+// attribute in either the definition or the declaration, and must match).
+//
+// There are cases when, even when building with XRay instrumentation, users
+// might want to control specifically which functions are instrumented for a
+// particular build using special-case lists provided to the compiler. These
+// special case lists are provided to Clang via the
+// -fxray-always-instrument=... and -fxray-never-instrument=... flags. The
+// attributes in source take precedence over these special-case lists.
+//
+// To disable the XRay attributes at build-time, users may define
+// ABSL_NO_XRAY_ATTRIBUTES. Do NOT define ABSL_NO_XRAY_ATTRIBUTES on specific
+// packages/targets, as this may lead to conflicting definitions of functions at
+// link-time.
+//
+#if ABSL_HAVE_CPP_ATTRIBUTE(clang::xray_always_instrument) && !defined(ABSL_NO_XRAY_ATTRIBUTES)
+#define ABSL_XRAY_ALWAYS_INSTRUMENT [[clang::xray_always_instrument]]
+#define ABSL_XRAY_NEVER_INSTRUMENT [[clang::xray_never_instrument]]
+#define ABSL_XRAY_LOG_ARGS(N) [[ clang::xray_always_instrument, clang::xray_log_args(N) ]]
+#else
+#define ABSL_XRAY_ALWAYS_INSTRUMENT
+#define ABSL_XRAY_NEVER_INSTRUMENT
+#define ABSL_XRAY_LOG_ARGS(N)
+#endif
+
+// -----------------------------------------------------------------------------
+// Variable Attributes
+// -----------------------------------------------------------------------------
+
+// ATTRIBUTE_UNUSED
+// Prevent the compiler from complaining about or optimizing away variables
+// that appear unused.
+#if ABSL_HAVE_ATTRIBUTE(unused) || (defined(__GNUC__) && !defined(__clang__))
+#undef ABSL_ATTRIBUTE_UNUSED
+#define ABSL_ATTRIBUTE_UNUSED __attribute__((__unused__))
+#else
+#define ABSL_ATTRIBUTE_UNUSED
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(unused) || (defined(__GNUC__) && !defined(__clang__))
+#undef ATTRIBUTE_UNUSED
+#define ATTRIBUTE_UNUSED __attribute__((__unused__))
+#else
+#define ATTRIBUTE_UNUSED
+#endif
+
+// ATTRIBUTE_INITIAL_EXEC
+// Tell the compiler to use "initial-exec" mode for a thread-local variable.
+// See http://people.redhat.com/drepper/tls.pdf for the gory details.
+#if ABSL_HAVE_ATTRIBUTE(tls_model) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_ATTRIBUTE_INITIAL_EXEC __attribute__((tls_model("initial-exec")))
+#else
+#define ABSL_ATTRIBUTE_INITIAL_EXEC
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(tls_model) || (defined(__GNUC__) && !defined(__clang__))
+#define ATTRIBUTE_INITIAL_EXEC __attribute__((tls_model("initial-exec")))
+#else
+#define ATTRIBUTE_INITIAL_EXEC
+#endif
+
+// ATTRIBUTE_PACKED
+// Prevent the compiler from padding a structure to natural alignment
+#if ABSL_HAVE_ATTRIBUTE(packed) || (defined(__GNUC__) && !defined(__clang__))
+#define ABSL_ATTRIBUTE_PACKED __attribute__((__packed__))
+#else
+#define ABSL_ATTRIBUTE_PACKED
+#endif
+
+// To be deleted macros. All macros are going te be renamed with ABSL_ prefix.
+#if ABSL_HAVE_ATTRIBUTE(packed) || (defined(__GNUC__) && !defined(__clang__))
+#define ATTRIBUTE_PACKED __attribute__((__packed__))
+#else
+#define ATTRIBUTE_PACKED
+#endif
+
+// ABSL_CONST_INIT
+// A variable declaration annotated with the ABSL_CONST_INIT attribute will
+// not compile (on supported platforms) unless the variable has a constant
+// initializer. This is useful for variables with static and thread storage
+// duration, because it guarantees that they will not suffer from the so-called
+// "static init order fiasco".
+//
+// Sample usage:
+//
+// ABSL_CONST_INIT static MyType my_var = MakeMyType(...);
+//
+// Note that this attribute is redundant if the variable is declared constexpr.
+#if ABSL_HAVE_CPP_ATTRIBUTE(clang::require_constant_initialization)
+// NOLINTNEXTLINE(whitespace/braces) (b/36288871)
+#define ABSL_CONST_INIT [[clang::require_constant_initialization]]
+#else
+#define ABSL_CONST_INIT
+#endif // ABSL_HAVE_CPP_ATTRIBUTE(clang::require_constant_initialization)
+
+#endif // THIRD_PARTY_ABSL_BASE_ATTRIBUTES_H_
diff --git a/Firestore/Port/absl/absl_config.h b/Firestore/Port/absl/absl_config.h
new file mode 100644
index 0000000..70f4d86
--- /dev/null
+++ b/Firestore/Port/absl/absl_config.h
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Defines preprocessor macros describing the presence of "features" available.
+// This facilitates writing portable code by parameterizing the compilation
+// based on the presence or lack of a feature.
+//
+// We define a feature as some interface we wish to program to: for example,
+// some library function or system call.
+//
+// For example, suppose a programmer wants to write a program that uses the
+// 'mmap' system call. Then one might write:
+//
+// #include "absl/base/config.h"
+//
+// #ifdef ABSL_HAVE_MMAP
+// #include "sys/mman.h"
+// #endif //ABSL_HAVE_MMAP
+//
+// ...
+// #ifdef ABSL_HAVE_MMAP
+// void *ptr = mmap(...);
+// ...
+// #endif // ABSL_HAVE_MMAP
+//
+// As a special note, using feature macros from config.h to determine whether
+// to include a particular header requires violating the style guide's required
+// ordering for headers: this is permitted.
+
+#ifndef THIRD_PARTY_ABSL_BASE_CONFIG_H_
+#define THIRD_PARTY_ABSL_BASE_CONFIG_H_
+
+// Included for the __GLIBC__ macro (or similar macros on other systems).
+#include <limits.h>
+
+#ifdef __cplusplus
+// Included for __GLIBCXX__, _LIBCPP_VERSION
+#include <cstddef>
+#endif // __cplusplus
+
+// If we're using glibc, make sure we meet a minimum version requirement
+// before we proceed much further.
+//
+// We have chosen glibc 2.12 as the minimum as it was tagged for release
+// in May, 2010 and includes some functionality used in Google software
+// (for instance pthread_setname_np):
+// https://sourceware.org/ml/libc-alpha/2010-05/msg00000.html
+#ifdef __GLIBC_PREREQ
+#if !__GLIBC_PREREQ(2, 12)
+#error "Minimum required version of glibc is 2.12."
+#endif
+#endif
+
+// ABSL_HAVE_BUILTIN is a function-like feature checking macro.
+// It's a wrapper around __has_builtin, which is defined by only clang now.
+// It evaluates to 1 if the builtin is supported or 0 if not.
+// Define it to avoid an extra level of #ifdef __has_builtin check.
+// http://releases.llvm.org/3.3/tools/clang/docs/LanguageExtensions.html
+#ifdef __has_builtin
+#define ABSL_HAVE_BUILTIN(x) __has_builtin(x)
+#else
+#define ABSL_HAVE_BUILTIN(x) 0
+#endif
+
+// ABSL_HAVE_STD_IS_TRIVIALLY_DESTRUCTIBLE is defined when
+// std::is_trivially_destructible<T> is supported.
+//
+// All supported compilers using libc++ have it, as does gcc >= 4.8
+// using libstdc++, as does Visual Studio.
+// https://gcc.gnu.org/onlinedocs/gcc-4.8.1/libstdc++/manual/manual/status.html#status.iso.2011
+// is the first version where std::is_trivially_destructible no longer
+// appeared as missing in the Type properties row.
+#ifdef ABSL_HAVE_STD_IS_TRIVIALLY_DESTRUCTIBLE
+#error ABSL_HAVE_STD_IS_TRIVIALLY_DESTRUCTIBLE cannot be directly set
+#elif defined(_LIBCPP_VERSION) || \
+ (!defined(__clang__) && defined(__GNUC__) && defined(__GLIBCXX__) && \
+ (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8))) || \
+ defined(_MSC_VER)
+#define ABSL_HAVE_STD_IS_TRIVIALLY_DESTRUCTIBLE 1
+#endif
+
+// ABSL_HAVE_STD_IS_TRIVIALLY_CONSTRUCTIBLE is defined when
+// std::is_trivially_default_constructible<T> and
+// std::is_trivially_copy_constructible<T> are supported.
+//
+// ABSL_HAVE_STD_IS_TRIVIALLY_ASSIGNABLE is defined when
+// std::is_trivially_copy_assignable<T> is supported.
+//
+// Clang with libc++ supports it, as does gcc >= 5.1 with either
+// libc++ or libstdc++, as does Visual Studio.
+// https://gcc.gnu.org/gcc-5/changes.html lists as new
+// "std::is_trivially_constructible, std::is_trivially_assignable
+// etc."
+#if defined(ABSL_HAVE_STD_IS_TRIVIALLY_CONSTRUCTIBLE)
+#error ABSL_HAVE_STD_IS_TRIVIALLY_CONSTRUCTIBLE cannot be directly set
+#elif defined(ABSL_HAVE_STD_IS_TRIVIALLY_ASSIGNABLE)
+#error ABSL_HAVE_STD_IS_TRIVIALLY_ASSIGNABLE cannot directly set
+#elif (defined(__clang__) && defined(_LIBCPP_VERSION)) || \
+ (!defined(__clang__) && defined(__GNUC__) && \
+ (__GNUC__ > 5 || (__GNUC__ == 5 && __GNUC_MINOR__ >= 1)) && \
+ (defined(_LIBCPP_VERSION) || defined(__GLIBCXX__))) || \
+ defined(_MSC_VER)
+#define ABSL_HAVE_STD_IS_TRIVIALLY_CONSTRUCTIBLE 1
+#define ABSL_HAVE_STD_IS_TRIVIALLY_ASSIGNABLE 1
+#endif
+
+// ABSL_HAVE_THREAD_LOCAL is defined when C++11's thread_local is available.
+// Clang implements thread_local keyword but Xcode did not support the
+// implementation until Xcode 8.
+#ifdef ABSL_HAVE_THREAD_LOCAL
+#error ABSL_HAVE_THREAD_LOCAL cannot be directly set
+#elif !defined(__apple_build_version__) || __apple_build_version__ >= 8000042
+#define ABSL_HAVE_THREAD_LOCAL 1
+#endif
+
+// ABSL_HAVE_INTRINSIC_INT128 is defined when the implementation provides the
+// 128 bit integral type: __int128.
+//
+// __SIZEOF_INT128__ is defined by Clang and GCC when __int128 is supported.
+// Clang on ppc64 and aarch64 are exceptions where __int128 exists but has a
+// sporadic compiler crashing bug. Nvidia's nvcc also defines __GNUC__ and
+// __SIZEOF_INT128__ but not all versions that do this support __int128. Support
+// has been tested for versions >= 7.
+#ifdef ABSL_HAVE_INTRINSIC_INT128
+#error ABSL_HAVE_INTRINSIC_INT128 cannot be directly set
+#elif (defined(__clang__) && defined(__SIZEOF_INT128__) && !defined(__ppc64__) && \
+ !defined(__aarch64__)) || \
+ (defined(__CUDACC__) && defined(__SIZEOF_INT128__) && __CUDACC_VER__ >= 70000) || \
+ (!defined(__clang__) && !defined(__CUDACC__) && defined(__GNUC__) && \
+ defined(__SIZEOF_INT128__))
+#define ABSL_HAVE_INTRINSIC_INT128 1
+#endif
+
+// Operating system-specific features.
+//
+// Currently supported operating systems and associated preprocessor
+// symbols:
+//
+// Linux and Linux-derived __linux__
+// Android __ANDROID__ (implies __linux__)
+// Linux (non-Android) __linux__ && !__ANDROID__
+// Darwin (Mac OS X and iOS) __APPLE__ && __MACH__
+// Akaros (http://akaros.org) __ros__
+// Windows _WIN32
+// NaCL __native_client__
+// AsmJS __asmjs__
+// Fuschia __Fuchsia__
+//
+// Note that since Android defines both __ANDROID__ and __linux__, one
+// may probe for either Linux or Android by simply testing for __linux__.
+//
+
+// ABSL_HAVE_MMAP is defined when the system has an mmap(2) implementation
+// as defined in POSIX.1-2001.
+#ifdef ABSL_HAVE_MMAP
+#error ABSL_HAVE_MMAP cannot be directly set
+#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__ros__) || \
+ defined(__native_client__) || defined(__asmjs__) || defined(__Fuchsia__)
+#define ABSL_HAVE_MMAP 1
+#endif
+
+// ABSL_HAS_PTHREAD_GETSCHEDPARAM is defined when the system implements the
+// pthread_(get|set)schedparam(3) functions as defined in POSIX.1-2001.
+#ifdef ABSL_HAVE_PTHREAD_GETSCHEDPARAM
+#error ABSL_HAVE_PTHREAD_GETSCHEDPARAM cannot be directly set
+#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__ros__)
+#define ABSL_HAVE_PTHREAD_GETSCHEDPARAM 1
+#endif
+
+// ABSL_HAVE_SCHED_YIELD is defined when the system implements
+// sched_yield(2) as defined in POSIX.1-2001.
+#ifdef ABSL_HAVE_SCHED_YIELD
+#error ABSL_HAVE_SCHED_YIELD cannot be directly set
+#elif defined(__linux__) || defined(__ros__) || defined(__native_client__)
+#define ABSL_HAVE_SCHED_YIELD 1
+#endif
+
+// ABSL_HAVE_SEMAPHORE_H is defined when the system supports the <semaphore.h>
+// header and sem_open(3) family of functions as standardized in POSIX.1-2001.
+//
+// Note: While Apple does have <semaphore.h> for both iOS and macOS, it is
+// explicity deprecated and will cause build failures if enabled for those
+// systems. We side-step the issue by not defining it here for Apple platforms.
+#ifdef ABSL_HAVE_SEMAPHORE_H
+#error ABSL_HAVE_SEMAPHORE_H cannot be directly set
+#elif defined(__linux__) || defined(__ros__)
+#define ABSL_HAVE_SEMAPHORE_H 1
+#endif
+
+// Library-specific features.
+#ifdef ABSL_HAVE_ALARM
+#error ABSL_HAVE_ALARM cannot be directly set
+#elif defined(__GOOGLE_GRTE_VERSION__)
+// feature tests for Google's GRTE
+#define ABSL_HAVE_ALARM 1
+#elif defined(__GLIBC__)
+// feature test for glibc
+#define ABSL_HAVE_ALARM 1
+#elif defined(_MSC_VER)
+// feature tests for Microsoft's library
+#elif defined(__native_client__)
+#else
+// other standard libraries
+#define ABSL_HAVE_ALARM 1
+#endif
+
+#if defined(_STLPORT_VERSION)
+#error "STLPort is not supported."
+#endif
+
+// -----------------------------------------------------------------------------
+// Endianness
+// -----------------------------------------------------------------------------
+// Define ABSL_IS_LITTLE_ENDIAN, ABSL_IS_BIG_ENDIAN.
+// Some compilers or system headers provide macros to specify endianness.
+// Unfortunately, there is no standard for the names of the macros or even of
+// the header files.
+// Reference: https://sourceforge.net/p/predef/wiki/Endianness/
+#if defined(ABSL_IS_BIG_ENDIAN) || defined(ABSL_IS_LITTLE_ENDIAN)
+#error "ABSL_IS_(BIG|LITTLE)_ENDIAN cannot be directly set."
+
+#elif defined(__GLIBC__) || defined(__linux__)
+// Operating systems that use the GNU C library generally provide <endian.h>
+// containing __BYTE_ORDER, __LITTLE_ENDIAN, __BIG_ENDIAN.
+#include <endian.h>
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define ABSL_IS_LITTLE_ENDIAN 1
+#elif __BYTE_ORDER == __BIG_ENDIAN
+#define ABSL_IS_BIG_ENDIAN 1
+#else // __BYTE_ORDER != __LITTLE_ENDIAN && __BYTE_ORDER != __BIG_ENDIAN
+#error "Unknown endianness"
+#endif // __BYTE_ORDER
+
+#elif defined(__APPLE__) && defined(__MACH__)
+// Apple has <machine/endian.h> containing BYTE_ORDER, BIG_ENDIAN,
+// LITTLE_ENDIAN.
+#include <machine/endian.h> // NOLINT(build/include)
+
+#if BYTE_ORDER == LITTLE_ENDIAN
+#define ABSL_IS_LITTLE_ENDIAN 1
+#elif BYTE_ORDER == BIG_ENDIAN
+#define ABSL_IS_BIG_ENDIAN 1
+#else // BYTE_ORDER != LITTLE_ENDIAN && BYTE_ORDER != BIG_ENDIAN
+#error "Unknown endianness"
+#endif // BYTE_ORDER
+
+#elif defined(_WIN32)
+// Assume Windows is little-endian.
+#define ABSL_IS_LITTLE_ENDIAN 1
+
+#elif defined(__LITTLE_ENDIAN__) || defined(__ARMEL__) || defined(__THUMBEL__) || \
+ defined(__AARCH64EL__) || defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__)
+#define ABSL_IS_LITTLE_ENDIAN 1
+
+#elif defined(__BIG_ENDIAN__) || defined(__ARMEB__) || defined(__THUMBEB__) || \
+ defined(__AARCH64EB__) || defined(_MIPSEB) || defined(__MIPSEB) || defined(__MIPSEB__)
+#define ABSL_IS_BIG_ENDIAN 1
+
+#else
+#error "absl endian detection needs to be set up on your platform."
+#endif
+
+// ABSL_HAVE_EXCEPTIONS is defined when exceptions are enabled. Many
+// compilers support a "no exceptions" mode that disables exceptions.
+//
+// Generally, when ABSL_HAVE_EXCEPTIONS is not defined:
+//
+// - Code using `throw` and `try` may not compile.
+// - The `noexcept` specifier will still compile and behave as normal.
+// - The `noexcept` operator may still return `false`.
+//
+// For further details, consult the compiler's documentation.
+#ifdef ABSL_HAVE_EXCEPTIONS
+#error ABSL_HAVE_EXCEPTIONS cannot be directly set.
+
+#elif defined(__clang__)
+// TODO
+// Switch to using __cpp_exceptions when we no longer support versions < 3.6.
+// For details on this check, see:
+// https://goo.gl/PilDrJ
+#if defined(__EXCEPTIONS) && __has_feature(cxx_exceptions)
+#define ABSL_HAVE_EXCEPTIONS 1
+#endif // defined(__EXCEPTIONS) && __has_feature(cxx_exceptions)
+
+// Handle remaining special cases and default to exceptions being supported.
+#elif !(defined(__GNUC__) && (__GNUC__ < 5) && !defined(__EXCEPTIONS)) && \
+ !(defined(__GNUC__) && (__GNUC__ >= 5) && !defined(__cpp_exceptions)) && \
+ !(defined(_MSC_VER) && !defined(_CPPUNWIND))
+#define ABSL_HAVE_EXCEPTIONS 1
+#endif
+
+#endif // THIRD_PARTY_ABSL_BASE_CONFIG_H_
diff --git a/Firestore/Port/absl/absl_endian.h b/Firestore/Port/absl/absl_endian.h
new file mode 100644
index 0000000..3b3cf3c
--- /dev/null
+++ b/Firestore/Port/absl/absl_endian.h
@@ -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.
+ */
+
+#ifndef ABSL_BASE_INTERNAL_ENDIAN_H_
+#define ABSL_BASE_INTERNAL_ENDIAN_H_
+
+// The following guarantees declaration of the byte swap functions
+#ifdef _MSC_VER
+#include <stdlib.h> // NOLINT(build/include)
+#elif defined(__APPLE__) && defined(__MACH__)
+// Mac OS X / Darwin features
+#include <libkern/OSByteOrder.h>
+#elif defined(__GLIBC__)
+#include <byteswap.h> // IWYU pragma: export
+#endif
+
+#include <cstdint>
+#include "absl_port.h"
+
+namespace absl {
+
+// Use compiler byte-swapping intrinsics if they are available. 32-bit
+// and 64-bit versions are available in Clang and GCC as of GCC 4.3.0.
+// The 16-bit version is available in Clang and GCC only as of GCC 4.8.0.
+// For simplicity, we enable them all only for GCC 4.8.0 or later.
+#if defined(__clang__) || \
+ (defined(__GNUC__) && ((__GNUC__ == 4 && __GNUC_MINOR__ >= 8) || __GNUC__ >= 5))
+inline uint64_t gbswap_64(uint64_t host_int) {
+ return __builtin_bswap64(host_int);
+}
+inline uint32_t gbswap_32(uint32_t host_int) {
+ return __builtin_bswap32(host_int);
+}
+inline uint16 gbswap_16(uint16 host_int) {
+ return __builtin_bswap16(host_int);
+}
+
+#elif defined(_MSC_VER)
+inline uint64_t gbswap_64(uint64_t host_int) {
+ return _byteswap_uint64(host_int);
+}
+inline uint32_t gbswap_32(uint32_t host_int) {
+ return _byteswap_ulong(host_int);
+}
+inline uint16 gbswap_16(uint16 host_int) {
+ return _byteswap_ushort(host_int);
+}
+
+#elif defined(__APPLE__) && defined(__MACH__)
+inline uint64_t gbswap_64(uint64_t host_int) {
+ return OSSwapInt16(host_int);
+}
+inline uint32_t gbswap_32(uint32_t host_int) {
+ return OSSwapInt32(host_int);
+}
+inline uint16 gbswap_16(uint16 host_int) {
+ return OSSwapInt64(host_int);
+}
+
+#else
+inline uint64_t gbswap_64(uint64_t host_int) {
+#if defined(__GNUC__) && defined(__x86_64__) && !(defined(__APPLE__) && defined(__MACH__))
+ // Adapted from /usr/include/byteswap.h. Not available on Mac.
+ if (__builtin_constant_p(host_int)) {
+ return __bswap_constant_64(host_int);
+ } else {
+ register uint64_t result;
+ __asm__("bswap %0" : "=r"(result) : "0"(host_int));
+ return result;
+ }
+#elif defined(__GLIBC__)
+ return bswap_64(host_int);
+#else
+ return (((x & GG_ULONGLONG(0xFF)) << 56) | ((x & GG_ULONGLONG(0xFF00)) << 40) |
+ ((x & GG_ULONGLONG(0xFF0000)) << 24) | ((x & GG_ULONGLONG(0xFF000000)) << 8) |
+ ((x & GG_ULONGLONG(0xFF00000000)) >> 8) | ((x & GG_ULONGLONG(0xFF0000000000)) >> 24) |
+ ((x & GG_ULONGLONG(0xFF000000000000)) >> 40) |
+ ((x & GG_ULONGLONG(0xFF00000000000000)) >> 56));
+#endif // bswap_64
+}
+
+inline uint32_t gbswap_32(uint32_t host_int) {
+#if defined(__GLIBC__)
+ return bswap_32(host_int);
+#else
+ return (((x & 0xFF) << 24) | ((x & 0xFF00) << 8) | ((x & 0xFF0000) >> 8) |
+ ((x & 0xFF000000) >> 24));
+#endif
+}
+
+inline uint16 gbswap_16(uint16 host_int) {
+#if defined(__GLIBC__)
+ return bswap_16(host_int);
+#else
+ return (uint16)(((x & 0xFF) << 8) | ((x & 0xFF00) >> 8)); // NOLINT
+#endif
+}
+
+#endif // intrinics available
+
+#ifdef ABSL_IS_LITTLE_ENDIAN
+
+// Definitions for ntohl etc. that don't require us to include
+// netinet/in.h. We wrap gbswap_32 and gbswap_16 in functions rather
+// than just #defining them because in debug mode, gcc doesn't
+// correctly handle the (rather involved) definitions of bswap_32.
+// gcc guarantees that inline functions are as fast as macros, so
+// this isn't a performance hit.
+inline uint16 ghtons(uint16 x) {
+ return gbswap_16(x);
+}
+inline uint32_t ghtonl(uint32_t x) {
+ return gbswap_32(x);
+}
+inline uint64_t ghtonll(uint64_t x) {
+ return gbswap_64(x);
+}
+
+#elif defined ABSL_IS_BIG_ENDIAN
+
+// These definitions are simpler on big-endian machines
+// These are functions instead of macros to avoid self-assignment warnings
+// on calls such as "i = ghtnol(i);". This also provides type checking.
+inline uint16 ghtons(uint16 x) {
+ return x;
+}
+inline uint32_t ghtonl(uint32_t x) {
+ return x;
+}
+inline uint64_t ghtonll(uint64_t x) {
+ return x;
+}
+
+#else
+#error \
+ "Unsupported byte order: Either ABSL_IS_BIG_ENDIAN or " \
+ "ABSL_IS_LITTLE_ENDIAN must be defined"
+#endif // byte order
+
+inline uint16 gntohs(uint16 x) {
+ return ghtons(x);
+}
+inline uint32_t gntohl(uint32_t x) {
+ return ghtonl(x);
+}
+inline uint64_t gntohll(uint64_t x) {
+ return ghtonll(x);
+}
+
+// Utilities to convert numbers between the current hosts's native byte
+// order and little-endian byte order
+//
+// Load/Store methods are alignment safe
+namespace little_endian {
+// Conversion functions.
+#ifdef ABSL_IS_LITTLE_ENDIAN
+
+inline uint16 FromHost16(uint16 x) {
+ return x;
+}
+inline uint16 ToHost16(uint16 x) {
+ return x;
+}
+
+inline uint32_t FromHost32(uint32_t x) {
+ return x;
+}
+inline uint32_t ToHost32(uint32_t x) {
+ return x;
+}
+
+inline uint64_t FromHost64(uint64_t x) {
+ return x;
+}
+inline uint64_t ToHost64(uint64_t x) {
+ return x;
+}
+
+inline constexpr bool IsLittleEndian() {
+ return true;
+}
+
+#elif defined ABSL_IS_BIG_ENDIAN
+
+inline uint16 FromHost16(uint16 x) {
+ return gbswap_16(x);
+}
+inline uint16 ToHost16(uint16 x) {
+ return gbswap_16(x);
+}
+
+inline uint32_t FromHost32(uint32_t x) {
+ return gbswap_32(x);
+}
+inline uint32_t ToHost32(uint32_t x) {
+ return gbswap_32(x);
+}
+
+inline uint64_t FromHost64(uint64_t x) {
+ return gbswap_64(x);
+}
+inline uint64_t ToHost64(uint64_t x) {
+ return gbswap_64(x);
+}
+
+inline constexpr bool IsLittleEndian() {
+ return false;
+}
+
+#endif /* ENDIAN */
+
+// Functions to do unaligned loads and stores in little-endian order.
+inline uint16 Load16(const void *p) {
+ return ToHost16(UNALIGNED_LOAD16(p));
+}
+
+inline void Store16(void *p, uint16 v) {
+ UNALIGNED_STORE16(p, FromHost16(v));
+}
+
+inline uint32_t Load32(const void *p) {
+ return ToHost32(UNALIGNED_LOAD32(p));
+}
+
+inline void Store32(void *p, uint32_t v) {
+ UNALIGNED_STORE32(p, FromHost32(v));
+}
+
+inline uint64_t Load64(const void *p) {
+ return ToHost64(UNALIGNED_LOAD64(p));
+}
+
+inline void Store64(void *p, uint64_t v) {
+ UNALIGNED_STORE64(p, FromHost64(v));
+}
+
+} // namespace little_endian
+
+// Utilities to convert numbers between the current hosts's native byte
+// order and big-endian byte order (same as network byte order)
+//
+// Load/Store methods are alignment safe
+namespace big_endian {
+#ifdef ABSL_IS_LITTLE_ENDIAN
+
+inline uint16 FromHost16(uint16 x) {
+ return gbswap_16(x);
+}
+inline uint16 ToHost16(uint16 x) {
+ return gbswap_16(x);
+}
+
+inline uint32_t FromHost32(uint32_t x) {
+ return gbswap_32(x);
+}
+inline uint32_t ToHost32(uint32_t x) {
+ return gbswap_32(x);
+}
+
+inline uint64_t FromHost64(uint64_t x) {
+ return gbswap_64(x);
+}
+inline uint64_t ToHost64(uint64_t x) {
+ return gbswap_64(x);
+}
+
+inline constexpr bool IsLittleEndian() {
+ return true;
+}
+
+#elif defined ABSL_IS_BIG_ENDIAN
+
+inline uint16 FromHost16(uint16 x) {
+ return x;
+}
+inline uint16 ToHost16(uint16 x) {
+ return x;
+}
+
+inline uint32_t FromHost32(uint32_t x) {
+ return x;
+}
+inline uint32_t ToHost32(uint32_t x) {
+ return x;
+}
+
+inline uint64_t FromHost64(uint64_t x) {
+ return x;
+}
+inline uint64_t ToHost64(uint64_t x) {
+ return x;
+}
+
+inline constexpr bool IsLittleEndian() {
+ return false;
+}
+
+#endif /* ENDIAN */
+
+// Functions to do unaligned loads and stores in big-endian order.
+inline uint16 Load16(const void *p) {
+ return ToHost16(UNALIGNED_LOAD16(p));
+}
+
+inline void Store16(void *p, uint16 v) {
+ UNALIGNED_STORE16(p, FromHost16(v));
+}
+
+inline uint32_t Load32(const void *p) {
+ return ToHost32(UNALIGNED_LOAD32(p));
+}
+
+inline void Store32(void *p, uint32_t v) {
+ UNALIGNED_STORE32(p, FromHost32(v));
+}
+
+inline uint64_t Load64(const void *p) {
+ return ToHost64(UNALIGNED_LOAD64(p));
+}
+
+inline void Store64(void *p, uint64_t v) {
+ UNALIGNED_STORE64(p, FromHost64(v));
+}
+
+} // namespace big_endian
+
+} // namespace absl
+
+#endif // ABSL_BASE_INTERNAL_ENDIAN_H_
diff --git a/Firestore/Port/absl/absl_integral_types.h b/Firestore/Port/absl/absl_integral_types.h
new file mode 100644
index 0000000..47da9c1
--- /dev/null
+++ b/Firestore/Port/absl/absl_integral_types.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.
+ */
+
+// Basic integer type definitions for various platforms
+//
+// This code is compiled directly on many platforms, including client
+// platforms like Windows, Mac, and embedded systems. Before making
+// any changes here, make sure that you're not breaking any platforms.
+//
+
+#ifndef THIRD_PARTY_ABSL_BASE_INTEGRAL_TYPES_H_
+#define THIRD_PARTY_ABSL_BASE_INTEGRAL_TYPES_H_
+
+// These typedefs are also defined in base/swig/google.swig. In the
+// SWIG environment, we use those definitions and avoid duplicate
+// definitions here with an ifdef. The definitions should be the
+// same in both files, and ideally be only defined in this file.
+#ifndef SWIG
+// Standard typedefs
+// Signed integer types with width of exactly 8, 16, 32, or 64 bits
+// respectively, for use when exact sizes are required.
+typedef signed char schar;
+typedef signed char int8;
+typedef short int16;
+typedef int int32;
+#ifdef COMPILER_MSVC
+typedef __int64 int64;
+#else
+typedef long long int64;
+#endif /* COMPILER_MSVC */
+
+// NOTE: unsigned types are DANGEROUS in loops and other arithmetical
+// places. Use the signed types unless your variable represents a bit
+// pattern (eg a hash value) or you really need the extra bit. Do NOT
+// use 'unsigned' to express "this value should always be positive";
+// use assertions for this.
+
+// Unsigned integer types with width of exactly 8, 16, 32, or 64 bits
+// respectively, for use when exact sizes are required.
+typedef unsigned char uint8;
+typedef unsigned short uint16;
+typedef unsigned int uint32;
+#ifdef COMPILER_MSVC
+typedef unsigned __int64 uint64;
+#else
+typedef unsigned long long uint64;
+#endif /* COMPILER_MSVC */
+
+// A type to represent a Unicode code-point value. As of Unicode 4.0,
+// such values require up to 21 bits.
+// (For type-checking on pointers, make this explicitly signed,
+// and it should always be the signed version of whatever int32 is.)
+typedef signed int char32;
+
+// A type to represent a natural machine word (for e.g. efficiently
+// scanning through memory for checksums or index searching). Don't use
+// this for storing normal integers. Ideally this would be just
+// unsigned int, but our 64-bit architectures use the LP64 model
+// (http://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models), hence
+// their ints are only 32 bits. We want to use the same fundamental
+// type on all archs if possible to preserve *printf() compatability.
+typedef unsigned long uword_t;
+
+#endif /* SWIG */
+
+// long long macros to be used because gcc and vc++ use different suffixes,
+// and different size specifiers in format strings
+#undef GG_LONGLONG
+#undef GG_ULONGLONG
+#undef GG_LL_FORMAT
+
+#ifdef COMPILER_MSVC /* if Visual C++ */
+
+// VC++ long long suffixes
+#define GG_LONGLONG(x) x##I64
+#define GG_ULONGLONG(x) x##UI64
+
+// Length modifier in printf format std::string for int64's (e.g. within %d)
+#define GG_LL_FORMAT "I64" // As in printf("%I64d", ...)
+#define GG_LL_FORMAT_W L"I64"
+
+#else /* not Visual C++ */
+
+#define GG_LONGLONG(x) x##LL
+#define GG_ULONGLONG(x) x##ULL
+#define GG_LL_FORMAT "ll" // As in "%lld". Note that "q" is poor form also.
+#define GG_LL_FORMAT_W L"ll"
+
+#endif // COMPILER_MSVC
+
+// There are still some requirements that we build these headers in
+// C-compatibility mode. Unfortunately, -Wall doesn't like c-style
+// casts, and C doesn't know how to read braced-initialization for
+// integers.
+#if defined(__cplusplus)
+const uint8 kuint8max{0xFF};
+const uint16 kuint16max{0xFFFF};
+const uint32 kuint32max{0xFFFFFFFF};
+const uint64 kuint64max{GG_ULONGLONG(0xFFFFFFFFFFFFFFFF)};
+const int8 kint8min{~0x7F};
+const int8 kint8max{0x7F};
+const int16 kint16min{~0x7FFF};
+const int16 kint16max{0x7FFF};
+const int32 kint32min{~0x7FFFFFFF};
+const int32 kint32max{0x7FFFFFFF};
+const int64 kint64min{GG_LONGLONG(~0x7FFFFFFFFFFFFFFF)};
+const int64 kint64max{GG_LONGLONG(0x7FFFFFFFFFFFFFFF)};
+#else // not __cplusplus, this branch exists only for C-compat
+static const uint8 kuint8max = ((uint8)0xFF);
+static const uint16 kuint16max = ((uint16)0xFFFF);
+static const uint32 kuint32max = ((uint32)0xFFFFFFFF);
+static const uint64 kuint64max = ((uint64)GG_LONGLONG(0xFFFFFFFFFFFFFFFF));
+static const int8 kint8min = ((int8)~0x7F);
+static const int8 kint8max = ((int8)0x7F);
+static const int16 kint16min = ((int16)~0x7FFF);
+static const int16 kint16max = ((int16)0x7FFF);
+static const int32 kint32min = ((int32)~0x7FFFFFFF);
+static const int32 kint32max = ((int32)0x7FFFFFFF);
+static const int64 kint64min = ((int64)GG_LONGLONG(~0x7FFFFFFFFFFFFFFF));
+static const int64 kint64max = ((int64)GG_LONGLONG(0x7FFFFFFFFFFFFFFF));
+#endif // __cplusplus
+
+// The following are not real constants, but we list them so CodeSearch and
+// other tools find them, in case people are looking for the above constants
+// under different names:
+// kMaxUint8, kMaxUint16, kMaxUint32, kMaxUint64
+// kMinInt8, kMaxInt8, kMinInt16, kMaxInt16, kMinInt32, kMaxInt32,
+// kMinInt64, kMaxInt64
+
+// No object has kIllegalFprint as its Fingerprint.
+typedef uint64 Fprint;
+static const Fprint kIllegalFprint = 0;
+static const Fprint kMaxFprint = GG_ULONGLONG(0xFFFFFFFFFFFFFFFF);
+
+#endif // THIRD_PARTY_ABSL_BASE_INTEGRAL_TYPES_H_
diff --git a/Firestore/Port/absl/absl_port.h b/Firestore/Port/absl/absl_port.h
new file mode 100644
index 0000000..3a123a2
--- /dev/null
+++ b/Firestore/Port/absl/absl_port.h
@@ -0,0 +1,535 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Various portability macros, type definitions, and inline functions
+// This file is used for both C and C++!
+//
+// These are weird things we need to do to get this compiling on
+// random systems (and on SWIG).
+//
+// This files is structured into the following high-level categories:
+// - Platform checks (OS, Compiler, C++, Library)
+// - Feature macros
+// - Utility macros
+// - Utility functions
+// - Type alias
+// - Predefined system/language macros
+// - Predefined system/language functions
+// - Compiler attributes (__attribute__)
+// - Performance optimization (alignment, branch prediction)
+// - Obsolete
+//
+
+#ifndef THIRD_PARTY_ABSL_BASE_PORT_H_
+#define THIRD_PARTY_ABSL_BASE_PORT_H_
+
+#include <assert.h>
+#include <limits.h> // So we can set the bounds of our types
+#include <stdlib.h> // for free()
+#include <string.h> // for memcpy()
+
+#include "absl_attributes.h"
+#include "absl_config.h"
+#include "absl_integral_types.h"
+
+#ifdef SWIG
+%include "attributes.h"
+#endif
+
+// -----------------------------------------------------------------------------
+// Operating System Check
+// -----------------------------------------------------------------------------
+
+#if defined(__CYGWIN__)
+#error "Cygwin is not supported."
+#endif
+
+// -----------------------------------------------------------------------------
+// Compiler Check
+// -----------------------------------------------------------------------------
+
+// We support MSVC++ 14.0 update 2 and later.
+#if defined(_MSC_FULL_VER) && _MSC_FULL_VER < 190023918
+#error "This package requires Visual Studio 2015 Update 2 or higher"
+#endif
+
+// We support gcc 4.7 and later.
+#if defined(__GNUC__) && !defined(__clang__)
+#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 7)
+#error "This package requires gcc 4.7 or higher"
+#endif
+#endif
+
+// We support Apple Xcode clang 4.2.1 (version 421.11.65) and later.
+// This corresponds to Apple Xcode version 4.5.
+#if defined(__apple_build_version__) && __apple_build_version__ < 4211165
+#error "This package requires __apple_build_version__ of 4211165 or higher"
+#endif
+
+// -----------------------------------------------------------------------------
+// C++ Version Check
+// -----------------------------------------------------------------------------
+
+// Enforce C++11 as the minimum. Note that Visual Studio has not
+// advanced __cplusplus despite being good enough for our purposes, so
+// so we exempt it from the check.
+#if defined(__cplusplus) && !defined(_MSC_VER) && !defined(SWIG)
+#if __cplusplus < 201103L
+#error "C++ versions less than C++11 are not supported."
+#endif
+#endif
+
+// -----------------------------------------------------------------------------
+// C++ Standard Library Check
+// -----------------------------------------------------------------------------
+
+#if defined(__cplusplus)
+#include <cstddef>
+#if defined(_STLPORT_VERSION)
+#error "STLPort is not supported."
+#endif
+#endif
+
+// -----------------------------------------------------------------------------
+// Feature Macros
+// -----------------------------------------------------------------------------
+
+// ABSL_HAVE_TLS is defined to 1 when __thread should be supported.
+// We assume __thread is supported on Linux when compiled with Clang or compiled
+// against libstdc++ with _GLIBCXX_HAVE_TLS defined.
+#ifdef ABSL_HAVE_TLS
+#error ABSL_HAVE_TLS cannot be directly set
+#elif defined(__linux__) && (defined(__clang__) || defined(_GLIBCXX_HAVE_TLS))
+#define ABSL_HAVE_TLS 1
+#endif
+
+// -----------------------------------------------------------------------------
+// Utility Macros
+// -----------------------------------------------------------------------------
+
+// ABSL_FUNC_PTR_TO_CHAR_PTR
+// On some platforms, a "function pointer" points to a function descriptor
+// rather than directly to the function itself.
+// Use ABSL_FUNC_PTR_TO_CHAR_PTR(func) to get a char-pointer to the first
+// instruction of the function func.
+#if defined(__cplusplus)
+#if (defined(__powerpc__) && !(_CALL_ELF > 1)) || defined(__ia64)
+// use opd section for function descriptors on these platforms, the function
+// address is the first word of the descriptor
+namespace absl {
+enum { kPlatformUsesOPDSections = 1 };
+} // namespace absl
+#define ABSL_FUNC_PTR_TO_CHAR_PTR(func) (reinterpret_cast<char **>(func)[0])
+#else // not PPC or IA64
+namespace absl {
+enum { kPlatformUsesOPDSections = 0 };
+} // namespace absl
+#define ABSL_FUNC_PTR_TO_CHAR_PTR(func) (reinterpret_cast<char *>(func))
+#endif // PPC or IA64
+#endif // __cplusplus
+
+// -----------------------------------------------------------------------------
+// Utility Functions
+// -----------------------------------------------------------------------------
+
+#if defined(__cplusplus)
+namespace absl {
+constexpr char PathSeparator() {
+#ifdef _WIN32
+ return '\\';
+#else
+ return '/';
+#endif
+}
+} // namespace absl
+#endif // __cplusplus
+
+// -----------------------------------------------------------------------------
+// Type Alias
+// -----------------------------------------------------------------------------
+
+#ifdef _MSC_VER
+// uid_t
+// MSVC doesn't have uid_t
+typedef int uid_t;
+
+// pid_t
+// Defined all over the place.
+typedef int pid_t;
+
+// ssize_t
+// VC++ doesn't understand "ssize_t". SSIZE_T is defined in <basetsd.h>.
+#include <basetsd.h>
+typedef SSIZE_T ssize_t;
+#endif // _MSC_VER
+
+// -----------------------------------------------------------------------------
+// Predefined System/Language Macros
+// -----------------------------------------------------------------------------
+
+// MAP_ANONYMOUS
+#if defined(__APPLE__) && defined(__MACH__)
+// For mmap, Linux defines both MAP_ANONYMOUS and MAP_ANON and says MAP_ANON is
+// deprecated. In Darwin, MAP_ANON is all there is.
+#if !defined MAP_ANONYMOUS
+#define MAP_ANONYMOUS MAP_ANON
+#endif // !MAP_ANONYMOUS
+#endif // __APPLE__ && __MACH__
+
+// PATH_MAX
+// You say tomato, I say atotom
+#ifdef _MSC_VER
+#define PATH_MAX MAX_PATH
+#endif
+
+// -----------------------------------------------------------------------------
+// Performance Optimization
+// -----------------------------------------------------------------------------
+
+// Alignment
+
+// CACHELINE_SIZE, CACHELINE_ALIGNED
+// Deprecated: Use ABSL_CACHELINE_SIZE, ABSL_CACHELINE_ALIGNED.
+// Note: When C++17 is available, consider using the following:
+// - std::hardware_constructive_interference_size
+// - std::hardware_destructive_interference_size
+// See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0154r1.html
+#if defined(__GNUC__)
+#if defined(__i386__) || defined(__x86_64__)
+#define CACHELINE_SIZE 64
+#define ABSL_CACHELINE_SIZE 64
+#elif defined(__powerpc64__)
+#define CACHELINE_SIZE 128
+#define ABSL_CACHELINE_SIZE 128
+#elif defined(__aarch64__)
+// We would need to read special register ctr_el0 to find out L1 dcache size.
+// This value is a good estimate based on a real aarch64 machine.
+#define CACHELINE_SIZE 64
+#define ABSL_CACHELINE_SIZE 64
+#elif defined(__arm__)
+// Cache line sizes for ARM: These values are not strictly correct since
+// cache line sizes depend on implementations, not architectures. There
+// are even implementations with cache line sizes configurable at boot
+// time.
+#if defined(__ARM_ARCH_5T__)
+#define CACHELINE_SIZE 32
+#define ABSL_CACHELINE_SIZE 32
+#elif defined(__ARM_ARCH_7A__)
+#define CACHELINE_SIZE 64
+#define ABSL_CACHELINE_SIZE 64
+#endif
+#endif
+
+#ifndef CACHELINE_SIZE
+// A reasonable default guess. Note that overestimates tend to waste more
+// space, while underestimates tend to waste more time.
+#define CACHELINE_SIZE 64
+#define ABSL_CACHELINE_SIZE 64
+#endif
+
+// On some compilers, expands to __attribute__((aligned(CACHELINE_SIZE))).
+// For compilers where this is not known to work, expands to nothing.
+//
+// No further guarantees are made here. The result of applying the macro
+// to variables and types is always implementation defined.
+//
+// WARNING: It is easy to use this attribute incorrectly, even to the point
+// of causing bugs that are difficult to diagnose, crash, etc. It does not
+// guarantee that objects are aligned to a cache line.
+//
+// Recommendations:
+//
+// 1) Consult compiler documentation; this comment is not kept in sync as
+// toolchains evolve.
+// 2) Verify your use has the intended effect. This often requires inspecting
+// the generated machine code.
+// 3) Prefer applying this attribute to individual variables. Avoid
+// applying it to types. This tends to localize the effect.
+#define CACHELINE_ALIGNED __attribute__((aligned(CACHELINE_SIZE)))
+#define ABSL_CACHELINE_ALIGNED __attribute__((aligned(ABSL_CACHELINE_SIZE)))
+
+#else // not GCC
+#define CACHELINE_SIZE 64
+#define ABSL_CACHELINE_SIZE 64
+#define CACHELINE_ALIGNED
+#define ABSL_CACHELINE_ALIGNED
+#endif
+
+// unaligned APIs
+
+// Portable handling of unaligned loads, stores, and copies.
+// On some platforms, like ARM, the copy functions can be more efficient
+// then a load and a store.
+//
+// It is possible to implement all of these these using constant-length memcpy
+// calls, which is portable and will usually be inlined into simple loads and
+// stores if the architecture supports it. However, such inlining usually
+// happens in a pass that's quite late in compilation, which means the resulting
+// loads and stores cannot participate in many other optimizations, leading to
+// overall worse code.
+
+// The unaligned API is C++ only. The declarations use C++ features
+// (namespaces, inline) which are absent or incompatible in C.
+#if defined(__cplusplus)
+
+#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || defined(MEMORY_SANITIZER)
+// Consider we have an unaligned load/store of 4 bytes from address 0x...05.
+// AddressSanitizer will treat it as a 3-byte access to the range 05:07 and
+// will miss a bug if 08 is the first unaddressable byte.
+// ThreadSanitizer will also treat this as a 3-byte access to 05:07 and will
+// miss a race between this access and some other accesses to 08.
+// MemorySanitizer will correctly propagate the shadow on unaligned stores
+// and correctly report bugs on unaligned loads, but it may not properly
+// update and report the origin of the uninitialized memory.
+// For all three tools, replacing an unaligned access with a tool-specific
+// callback solves the problem.
+
+// Make sure uint16_t/uint32_t/uint64_t are defined.
+#include <stdint.h>
+
+extern "C" {
+uint16_t __sanitizer_unaligned_load16(const void *p);
+uint32_t __sanitizer_unaligned_load32(const void *p);
+uint64_t __sanitizer_unaligned_load64(const void *p);
+void __sanitizer_unaligned_store16(void *p, uint16_t v);
+void __sanitizer_unaligned_store32(void *p, uint32_t v);
+void __sanitizer_unaligned_store64(void *p, uint64_t v);
+} // extern "C"
+
+inline uint16 UNALIGNED_LOAD16(const void *p) {
+ return __sanitizer_unaligned_load16(p);
+}
+
+inline uint32 UNALIGNED_LOAD32(const void *p) {
+ return __sanitizer_unaligned_load32(p);
+}
+
+inline uint64 UNALIGNED_LOAD64(const void *p) {
+ return __sanitizer_unaligned_load64(p);
+}
+
+inline void UNALIGNED_STORE16(void *p, uint16 v) {
+ __sanitizer_unaligned_store16(p, v);
+}
+
+inline void UNALIGNED_STORE32(void *p, uint32 v) {
+ __sanitizer_unaligned_store32(p, v);
+}
+
+inline void UNALIGNED_STORE64(void *p, uint64 v) {
+ __sanitizer_unaligned_store64(p, v);
+}
+
+#elif defined(__x86_64__) || defined(_M_X64) || defined(__i386) || defined(_M_IX86) || \
+ defined(__ppc__) || defined(__PPC__) || defined(__ppc64__) || defined(__PPC64__)
+
+// x86 and x86-64 can perform unaligned loads/stores directly;
+// modern PowerPC hardware can also do unaligned integer loads and stores;
+// but note: the FPU still sends unaligned loads and stores to a trap handler!
+
+#define UNALIGNED_LOAD16(_p) (*reinterpret_cast<const uint16 *>(_p))
+#define UNALIGNED_LOAD32(_p) (*reinterpret_cast<const uint32 *>(_p))
+#define UNALIGNED_LOAD64(_p) (*reinterpret_cast<const uint64 *>(_p))
+
+#define UNALIGNED_STORE16(_p, _val) (*reinterpret_cast<uint16 *>(_p) = (_val))
+#define UNALIGNED_STORE32(_p, _val) (*reinterpret_cast<uint32 *>(_p) = (_val))
+#define UNALIGNED_STORE64(_p, _val) (*reinterpret_cast<uint64 *>(_p) = (_val))
+
+#elif defined(__arm__) && !defined(__ARM_ARCH_5__) && !defined(__ARM_ARCH_5T__) && \
+ !defined(__ARM_ARCH_5TE__) && !defined(__ARM_ARCH_5TEJ__) && !defined(__ARM_ARCH_6__) && \
+ !defined(__ARM_ARCH_6J__) && !defined(__ARM_ARCH_6K__) && !defined(__ARM_ARCH_6Z__) && \
+ !defined(__ARM_ARCH_6ZK__) && !defined(__ARM_ARCH_6T2__)
+
+// ARMv7 and newer support native unaligned accesses, but only of 16-bit
+// and 32-bit values (not 64-bit); older versions either raise a fatal signal,
+// do an unaligned read and rotate the words around a bit, or do the reads very
+// slowly (trip through kernel mode). There's no simple #define that says just
+// “ARMv7 or higher”, so we have to filter away all ARMv5 and ARMv6
+// sub-architectures. Newer gcc (>= 4.6) set an __ARM_FEATURE_ALIGNED #define,
+// so in time, maybe we can move on to that.
+//
+// This is a mess, but there's not much we can do about it.
+//
+// To further complicate matters, only LDR instructions (single reads) are
+// allowed to be unaligned, not LDRD (two reads) or LDM (many reads). Unless we
+// explicitly tell the compiler that these accesses can be unaligned, it can and
+// will combine accesses. On armcc, the way to signal this is done by accessing
+// through the type (uint32 __packed *), but GCC has no such attribute
+// (it ignores __attribute__((packed)) on individual variables). However,
+// we can tell it that a _struct_ is unaligned, which has the same effect,
+// so we do that.
+
+namespace base {
+namespace internal {
+
+struct Unaligned16Struct {
+ uint16 value;
+ uint8 dummy; // To make the size non-power-of-two.
+} ATTRIBUTE_PACKED;
+
+struct Unaligned32Struct {
+ uint32 value;
+ uint8 dummy; // To make the size non-power-of-two.
+} ATTRIBUTE_PACKED;
+
+} // namespace internal
+} // namespace base
+
+#define UNALIGNED_LOAD16(_p) \
+ ((reinterpret_cast<const ::base::internal::Unaligned16Struct *>(_p))->value)
+#define UNALIGNED_LOAD32(_p) \
+ ((reinterpret_cast<const ::base::internal::Unaligned32Struct *>(_p))->value)
+
+#define UNALIGNED_STORE16(_p, _val) \
+ ((reinterpret_cast< ::base::internal::Unaligned16Struct *>(_p))->value = (_val))
+#define UNALIGNED_STORE32(_p, _val) \
+ ((reinterpret_cast< ::base::internal::Unaligned32Struct *>(_p))->value = (_val))
+
+inline uint64 UNALIGNED_LOAD64(const void *p) {
+ uint64 t;
+ memcpy(&t, p, sizeof t);
+ return t;
+}
+
+inline void UNALIGNED_STORE64(void *p, uint64 v) {
+ memcpy(p, &v, sizeof v);
+}
+
+#else
+
+#define NEED_ALIGNED_LOADS
+
+// These functions are provided for architectures that don't support
+// unaligned loads and stores.
+
+inline uint16 UNALIGNED_LOAD16(const void *p) {
+ uint16 t;
+ memcpy(&t, p, sizeof t);
+ return t;
+}
+
+inline uint32 UNALIGNED_LOAD32(const void *p) {
+ uint32 t;
+ memcpy(&t, p, sizeof t);
+ return t;
+}
+
+inline uint64 UNALIGNED_LOAD64(const void *p) {
+ uint64 t;
+ memcpy(&t, p, sizeof t);
+ return t;
+}
+
+inline void UNALIGNED_STORE16(void *p, uint16 v) {
+ memcpy(p, &v, sizeof v);
+}
+
+inline void UNALIGNED_STORE32(void *p, uint32 v) {
+ memcpy(p, &v, sizeof v);
+}
+
+inline void UNALIGNED_STORE64(void *p, uint64 v) {
+ memcpy(p, &v, sizeof v);
+}
+
+#endif
+
+// The UNALIGNED_LOADW and UNALIGNED_STOREW macros load and store values
+// of type uword_t.
+#ifdef _LP64
+#define UNALIGNED_LOADW(_p) UNALIGNED_LOAD64(_p)
+#define UNALIGNED_STOREW(_p, _val) UNALIGNED_STORE64(_p, _val)
+#else
+#define UNALIGNED_LOADW(_p) UNALIGNED_LOAD32(_p)
+#define UNALIGNED_STOREW(_p, _val) UNALIGNED_STORE32(_p, _val)
+#endif
+
+inline void UnalignedCopy16(const void *src, void *dst) {
+ UNALIGNED_STORE16(dst, UNALIGNED_LOAD16(src));
+}
+
+inline void UnalignedCopy32(const void *src, void *dst) {
+ UNALIGNED_STORE32(dst, UNALIGNED_LOAD32(src));
+}
+
+inline void UnalignedCopy64(const void *src, void *dst) {
+ if (sizeof(void *) == 8) {
+ UNALIGNED_STORE64(dst, UNALIGNED_LOAD64(src));
+ } else {
+ const char *src_char = reinterpret_cast<const char *>(src);
+ char *dst_char = reinterpret_cast<char *>(dst);
+
+ UNALIGNED_STORE32(dst_char, UNALIGNED_LOAD32(src_char));
+ UNALIGNED_STORE32(dst_char + 4, UNALIGNED_LOAD32(src_char + 4));
+ }
+}
+
+#endif // defined(__cplusplus), end of unaligned API
+
+// PREDICT_TRUE, PREDICT_FALSE
+//
+// GCC can be told that a certain branch is not likely to be taken (for
+// instance, a CHECK failure), and use that information in static analysis.
+// Giving it this information can help it optimize for the common case in
+// the absence of better information (ie. -fprofile-arcs).
+#if ABSL_HAVE_BUILTIN(__builtin_expect) || (defined(__GNUC__) && !defined(__clang__))
+#define PREDICT_FALSE(x) (__builtin_expect(x, 0))
+#define PREDICT_TRUE(x) (__builtin_expect(!!(x), 1))
+#define ABSL_PREDICT_FALSE(x) (__builtin_expect(x, 0))
+#define ABSL_PREDICT_TRUE(x) (__builtin_expect(!!(x), 1))
+#else
+#define PREDICT_FALSE(x) x
+#define PREDICT_TRUE(x) x
+#define ABSL_PREDICT_FALSE(x) x
+#define ABSL_PREDICT_TRUE(x) x
+#endif
+
+// ABSL_ASSERT
+//
+// In C++11, `assert` can't be used portably within constexpr functions.
+// ABSL_ASSERT functions as a runtime assert but works in C++11 constexpr
+// functions. Example:
+//
+// constexpr double Divide(double a, double b) {
+// return ABSL_ASSERT(b != 0), a / b;
+// }
+//
+// This macro is inspired by
+// https://akrzemi1.wordpress.com/2017/05/18/asserts-in-constexpr-functions/
+#if defined(NDEBUG)
+#define ABSL_ASSERT(expr) (false ? (void)(expr) : (void)0)
+#else
+#define ABSL_ASSERT(expr) \
+ (PREDICT_TRUE((expr)) ? (void)0 : [] { assert(false && #expr); }()) // NOLINT
+#endif
+
+// -----------------------------------------------------------------------------
+// Obsolete (to be removed)
+// -----------------------------------------------------------------------------
+
+// HAS_GLOBAL_STRING
+// Some platforms have a std::string class that is different from ::std::string
+// (although the interface is the same, of course). On other platforms,
+// std::string is the same as ::std::string.
+#if defined(__cplusplus) && !defined(SWIG)
+#include <string>
+#ifndef HAS_GLOBAL_STRING
+using std::basic_string;
+using std::string;
+#endif // HAS_GLOBAL_STRING
+#endif // SWIG, __cplusplus
+
+#endif // THIRD_PARTY_ABSL_BASE_PORT_H_
diff --git a/Firestore/Port/bits.cc b/Firestore/Port/bits.cc
new file mode 100644
index 0000000..40af964
--- /dev/null
+++ b/Firestore/Port/bits.cc
@@ -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.
+ */
+
+#include "bits.h"
+
+#include <assert.h>
+
+namespace Firestore {
+
+int Bits::Log2Floor_Portable(uint32_t n) {
+ if (n == 0) return -1;
+ int log = 0;
+ uint32_t value = n;
+ for (int i = 4; i >= 0; --i) {
+ int shift = (1 << i);
+ uint32_t x = value >> shift;
+ if (x != 0) {
+ value = x;
+ log += shift;
+ }
+ }
+ assert(value == 1);
+ return log;
+}
+
+} // namespace Firestore
diff --git a/Firestore/Port/bits.h b/Firestore/Port/bits.h
new file mode 100644
index 0000000..d212bf8
--- /dev/null
+++ b/Firestore/Port/bits.h
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 IPHONE_FIRESTORE_PORT_BITS_H_
+#define IPHONE_FIRESTORE_PORT_BITS_H_
+
+// Various bit-twiddling functions, all of which are static members of the Bits
+// class (making it effectively a namespace). Operands are unsigned integers.
+// Munging bits in _signed_ integers is fraught with peril! For example,
+// -5 << n has undefined behavior (for some values of n).
+
+#include <stdint.h>
+
+class Bits_Port32_Test;
+class Bits_Port64_Test;
+
+namespace Firestore {
+
+class Bits {
+ public:
+ // Return floor(log2(n)) for positive integer n. Returns -1 iff n == 0.
+ static int Log2Floor(uint32_t n);
+ static int Log2Floor64(uint64_t n);
+
+ // Potentially faster version of Log2Floor() that returns an
+ // undefined value if n == 0
+ static int Log2FloorNonZero(uint32_t n);
+ static int Log2FloorNonZero64(uint64_t n);
+
+ private:
+ // Portable implementations.
+ static int Log2Floor_Portable(uint32_t n);
+ static int Log2Floor64_Portable(uint64_t n);
+ static int Log2FloorNonZero_Portable(uint32_t n);
+ static int Log2FloorNonZero64_Portable(uint64_t n);
+
+ Bits(Bits const&) = delete;
+ void operator=(Bits const&) = delete;
+
+ // Allow tests to call _Portable variants directly.
+ friend class ::Bits_Port32_Test;
+ friend class ::Bits_Port64_Test;
+};
+
+// ------------------------------------------------------------------------
+// Implementation details follow
+// ------------------------------------------------------------------------
+
+#if defined(__GNUC__)
+
+inline int Bits::Log2Floor(uint32_t n) {
+ return n == 0 ? -1 : 31 ^ __builtin_clz(n);
+}
+
+inline int Bits::Log2FloorNonZero(uint32_t n) {
+ return 31 ^ __builtin_clz(n);
+}
+
+inline int Bits::Log2Floor64(uint64_t n) {
+ return n == 0 ? -1 : 63 ^ __builtin_clzll(n);
+}
+
+inline int Bits::Log2FloorNonZero64(uint64_t n) {
+ return 63 ^ __builtin_clzll(n);
+}
+
+#elif defined(COMPILER_MSVC)
+
+inline int Bits::Log2FloorNonZero(uint32 n) {
+#ifdef _M_IX86
+ _asm {
+ bsr ebx, n
+ mov n, ebx
+ }
+ return n;
+#else
+ return Bits::Log2FloorNonZero_Portable(n);
+#endif
+}
+
+inline int Bits::Log2Floor(uint32 n) {
+#ifdef _M_IX86
+ _asm {
+ xor ebx, ebx
+ mov eax, n
+ and eax, eax
+ jz return_ebx
+ bsr ebx, eax
+ return_ebx:
+ mov n, ebx
+ }
+ return n;
+#else
+ return Bits::Log2Floor_Portable(n);
+#endif
+}
+
+inline int Bits::Log2Floor64(uint64_t n) {
+ return Bits::Log2Floor64_Portable(n);
+}
+
+inline int Bits::Log2FloorNonZero64(uint64_t n) {
+ return Bits::Log2FloorNonZero64_Portable(n);
+}
+
+#else // !__GNUC__ && !COMPILER_MSVC
+
+inline int Bits::Log2Floor64(uint64_t n) {
+ return Bits::Log2Floor64_Portable(n);
+}
+
+inline int Bits::Log2FloorNonZero64(uint64_t n) {
+ return Bits::Log2FloorNonZero64_Portable(n);
+}
+
+#endif
+
+inline int Bits::Log2FloorNonZero_Portable(uint32_t n) {
+ // Just use the common routine
+ return Log2Floor(n);
+}
+
+// Log2Floor64() is defined in terms of Log2Floor32(), Log2FloorNonZero32()
+inline int Bits::Log2Floor64_Portable(uint64_t n) {
+ const uint32_t topbits = static_cast<uint32_t>(n >> 32);
+ if (topbits == 0) {
+ // Top bits are zero, so scan in bottom bits
+ return Log2Floor(static_cast<uint32_t>(n));
+ } else {
+ return 32 + Log2FloorNonZero(topbits);
+ }
+}
+
+// Log2FloorNonZero64() is defined in terms of Log2FloorNonZero32()
+inline int Bits::Log2FloorNonZero64_Portable(uint64_t n) {
+ const uint32_t topbits = static_cast<uint32_t>(n >> 32);
+ if (topbits == 0) {
+ // Top bits are zero, so scan in bottom bits
+ return Log2FloorNonZero(static_cast<uint32_t>(n));
+ } else {
+ return 32 + Log2FloorNonZero(topbits);
+ }
+}
+
+} // namespace Firestore
+
+#endif // IPHONE_FIRESTORE_PORT_BITS_H_
diff --git a/Firestore/Port/bits_test.cc b/Firestore/Port/bits_test.cc
new file mode 100644
index 0000000..18c3b1d
--- /dev/null
+++ b/Firestore/Port/bits_test.cc
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "bits.h"
+
+#include <iostream>
+
+#include "base/commandlineflags.h"
+#include "testing/base/public/gunit.h"
+#include "util/random/mt_random.h"
+
+using Firestore::Bits;
+
+DEFINE_int32(num_iterations, 10000, "Number of test iterations to run.");
+
+class BitsTest : public testing::Test {
+ public:
+ BitsTest() : random_(testing::FLAGS_gunit_random_seed) {}
+
+ protected:
+ MTRandom random_;
+};
+
+TEST_F(BitsTest, Log2EdgeCases) {
+ std::cout << "TestLog2EdgeCases" << std::endl;
+
+ EXPECT_EQ(-1, Bits::Log2Floor(0));
+ EXPECT_EQ(-1, Bits::Log2Floor64(0));
+
+ for (int i = 0; i < 32; i++) {
+ uint32 n = 1U << i;
+ EXPECT_EQ(i, Bits::Log2Floor(n));
+ EXPECT_EQ(i, Bits::Log2FloorNonZero(n));
+ if (n > 2) {
+ EXPECT_EQ(i - 1, Bits::Log2Floor(n - 1));
+ EXPECT_EQ(i, Bits::Log2Floor(n + 1));
+ EXPECT_EQ(i - 1, Bits::Log2FloorNonZero(n - 1));
+ EXPECT_EQ(i, Bits::Log2FloorNonZero(n + 1));
+ }
+ }
+
+ for (int i = 0; i < 64; i++) {
+ uint64 n = 1ULL << i;
+ EXPECT_EQ(i, Bits::Log2Floor64(n));
+ EXPECT_EQ(i, Bits::Log2FloorNonZero64(n));
+ if (n > 2) {
+ EXPECT_EQ(i - 1, Bits::Log2Floor64(n - 1));
+ EXPECT_EQ(i, Bits::Log2Floor64(n + 1));
+ EXPECT_EQ(i - 1, Bits::Log2FloorNonZero64(n - 1));
+ EXPECT_EQ(i, Bits::Log2FloorNonZero64(n + 1));
+ }
+ }
+}
+
+TEST_F(BitsTest, Log2Random) {
+ std::cout << "TestLog2Random" << std::endl;
+
+ for (int i = 0; i < FLAGS_num_iterations; i++) {
+ int maxbit = -1;
+ uint32 n = 0;
+ while (!random_.OneIn(32)) {
+ int bit = random_.Uniform(32);
+ n |= (1U << bit);
+ maxbit = std::max(bit, maxbit);
+ }
+ EXPECT_EQ(maxbit, Bits::Log2Floor(n));
+ if (n != 0) {
+ EXPECT_EQ(maxbit, Bits::Log2FloorNonZero(n));
+ }
+ }
+}
+
+TEST_F(BitsTest, Log2Random64) {
+ std::cout << "TestLog2Random64" << std::endl;
+
+ for (int i = 0; i < FLAGS_num_iterations; i++) {
+ int maxbit = -1;
+ uint64 n = 0;
+ while (!random_.OneIn(64)) {
+ int bit = random_.Uniform(64);
+ n |= (1ULL << bit);
+ maxbit = std::max(bit, maxbit);
+ }
+ EXPECT_EQ(maxbit, Bits::Log2Floor64(n));
+ if (n != 0) {
+ EXPECT_EQ(maxbit, Bits::Log2FloorNonZero64(n));
+ }
+ }
+}
+
+TEST(Bits, Port32) {
+ for (int shift = 0; shift < 32; shift++) {
+ for (int delta = -1; delta <= +1; delta++) {
+ const uint32 v = (static_cast<uint32>(1) << shift) + delta;
+ EXPECT_EQ(Bits::Log2Floor_Portable(v), Bits::Log2Floor(v)) << v;
+ if (v != 0) {
+ EXPECT_EQ(Bits::Log2FloorNonZero_Portable(v), Bits::Log2FloorNonZero(v))
+ << v;
+ }
+ }
+ }
+ static const uint32 M32 = kuint32max;
+ EXPECT_EQ(Bits::Log2Floor_Portable(M32), Bits::Log2Floor(M32)) << M32;
+ EXPECT_EQ(Bits::Log2FloorNonZero_Portable(M32), Bits::Log2FloorNonZero(M32))
+ << M32;
+}
+
+TEST(Bits, Port64) {
+ for (int shift = 0; shift < 64; shift++) {
+ for (int delta = -1; delta <= +1; delta++) {
+ const uint64 v = (static_cast<uint64>(1) << shift) + delta;
+ EXPECT_EQ(Bits::Log2Floor64_Portable(v), Bits::Log2Floor64(v)) << v;
+ if (v != 0) {
+ EXPECT_EQ(Bits::Log2FloorNonZero64_Portable(v),
+ Bits::Log2FloorNonZero64(v))
+ << v;
+ }
+ }
+ }
+ static const uint64 M64 = kuint64max;
+ EXPECT_EQ(Bits::Log2Floor64_Portable(M64), Bits::Log2Floor64(M64)) << M64;
+ EXPECT_EQ(Bits::Log2FloorNonZero64_Portable(M64),
+ Bits::Log2FloorNonZero64(M64))
+ << M64;
+}
diff --git a/Firestore/Port/ordered_code.cc b/Firestore/Port/ordered_code.cc
new file mode 100644
index 0000000..ec5733c
--- /dev/null
+++ b/Firestore/Port/ordered_code.cc
@@ -0,0 +1,579 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ordered_code.h"
+
+#include <assert.h>
+
+#include "bits.h"
+
+#include "absl_endian.h"
+#include "absl_port.h"
+#include <leveldb/db.h> // For Slice
+
+// We encode a string in different ways depending on whether the item
+// should be in lexicographically increasing or decreasing order.
+//
+//
+// Lexicographically increasing order
+//
+// We want a string-to-string mapping F(x) such that for any two strings
+//
+// x < y => F(x) < F(y)
+//
+// In addition to the normal characters '\x00' through '\xff', we want to
+// encode a few extra symbols in strings:
+//
+// <sep> Separator between items
+// <infinity> Infinite string
+//
+// Therefore we need an alphabet with at least 258 symbols. Each
+// character '\1' through '\xfe' is mapped to itself. The other four are
+// encoded into two-letter sequences starting with '\0' and '\xff':
+//
+// <sep> encoded as => \0\1
+// \0 encoded as => \0\xff
+// \xff encoded as => \xff\x00
+// <infinity> encoded as => \xff\xff
+//
+// The remaining two-letter sequences starting with '\0' and '\xff' are
+// currently unused.
+//
+// F(<infinity>) is defined above. For any finite string x, F(x) is the
+// the encodings of x's characters followed by the encoding for <sep>. The
+// ordering of two finite strings is the same as the ordering of the
+// respective characters at the first position where they differ, which in
+// turn is the same as the ordering of the encodings of those two
+// characters. Moreover, for every finite string x, F(x) < F(<infinity>).
+
+namespace Firestore {
+
+using leveldb::Slice;
+
+static const char kEscape1 = '\000';
+static const char kNullCharacter = '\xff'; // Combined with kEscape1
+static const char kSeparator = '\001'; // Combined with kEscape1
+
+static const char kEscape2 = '\xff';
+static const char kInfinity = '\xff'; // Combined with kEscape2
+static const char kFFCharacter = '\000'; // Combined with kEscape2
+
+static const char kEscape1_Separator[2] = {kEscape1, kSeparator};
+
+// Append to "*dest" the "len" bytes starting from "*src".
+inline static void AppendBytes(std::string* dest, const char* src, size_t len) {
+ dest->append(src, len);
+}
+
+inline bool IsSpecialByte(char c) { return ((unsigned char)(c + 1)) < 2; }
+
+// Returns 0 if one or more of the bytes in the specified uint32 value
+// are the special values 0 or 255, and returns 4 otherwise. The
+// result of this routine can be added to "p" to either advance past
+// the next 4 bytes if they do not contain a special byte, or to
+// remain on this set of four bytes if they contain the next special
+// byte occurrence.
+//
+// REQUIRES: v is the value of loading the next 4 bytes from "*p" (we
+// pass in v rather than loading it because in some cases, the client
+// may already have the value in a register: "p" is just used for
+// assertion checking).
+inline int AdvanceIfNoSpecialBytes(uint32_t v_32, const char* p) {
+ assert(UNALIGNED_LOAD32(p) == v_32);
+ // See comments in SkipToNextSpecialByte if you wish to
+ // understand this expression (which checks for the occurrence
+ // of the special byte values 0 or 255 in any of the bytes of v_32).
+ if ((v_32 - 0x01010101u) & ~(v_32 + 0x01010101u) & 0x80808080u) {
+ // Special byte is in p[0..3]
+ assert(IsSpecialByte(p[0]) || IsSpecialByte(p[1]) || IsSpecialByte(p[2]) ||
+ IsSpecialByte(p[3]));
+ return 0;
+ } else {
+ assert(!IsSpecialByte(p[0]));
+ assert(!IsSpecialByte(p[1]));
+ assert(!IsSpecialByte(p[2]));
+ assert(!IsSpecialByte(p[3]));
+ return 4;
+ }
+}
+
+// Return a pointer to the first byte in the range "[start..limit)"
+// whose value is 0 or 255 (kEscape1 or kEscape2). If no such byte
+// exists in the range, returns "limit".
+inline const char* SkipToNextSpecialByte(const char* start, const char* limit) {
+ // If these constants were ever changed, this routine needs to change
+ assert(kEscape1 == 0);
+ assert((kEscape2 & 0xffu) == 255u);
+ const char* p = start;
+ while (p + 8 <= limit) {
+ // Find out if any of the next 8 bytes are either 0 or 255 (our
+ // two characters that require special handling). We do this using
+ // the technique described in:
+ //
+ // http://graphics.stanford.edu/~seander/bithacks.html#HasLessInWord
+ //
+ // We use the test (x + 1) < 2 to check x = 0 or -1(255)
+ //
+ // If x is a byte value (0x00..0xff):
+ // (x - 0x01) & 0x80 is true only when x = 0x81..0xff, 0x00
+ // ~(x + 0x01) & 0x80 is true only when x = 0x00..0x7e, 0xff
+ // The intersection of the above two sets is x = 0x00 or 0xff.
+ // We can ignore carry between bytes because only x = 0x00 or 0xff
+ // can cause carry in the expression -- and such x already makes the
+ // result value non-zero.
+ uint64_t v = UNALIGNED_LOAD64(p);
+ bool hasZeroOr255Byte = (v - 0x0101010101010101ull) &
+ ~(v + 0x0101010101010101ull) &
+ 0x8080808080808080ull;
+ if (!hasZeroOr255Byte) {
+ // No special values in the next 8 bytes
+ p += 8;
+ } else {
+// We know the next 8 bytes have a special byte: find it
+#ifdef IS_LITTLE_ENDIAN
+ uint32_t v_32 = static_cast<uint32_t>(v); // Low 32 bits of v
+#else
+ uint32_t v_32 = UNALIGNED_LOAD32(p);
+#endif
+ // Test 32 bits at once to see if special byte is in next 4 bytes
+ // or the following 4 bytes
+ p += AdvanceIfNoSpecialBytes(v_32, p);
+ if (IsSpecialByte(p[0])) return p;
+ if (IsSpecialByte(p[1])) return p + 1;
+ if (IsSpecialByte(p[2])) return p + 2;
+ assert(IsSpecialByte(p[3])); // Last byte must be the special one
+ return p + 3;
+ }
+ }
+ if (p + 4 <= limit) {
+ uint32_t v_32 = UNALIGNED_LOAD32(p);
+ p += AdvanceIfNoSpecialBytes(v_32, p);
+ }
+ while (p < limit && !IsSpecialByte(*p)) {
+ p++;
+ }
+ return p;
+}
+
+// Expose SkipToNextSpecialByte for testing purposes
+const char* OrderedCode::TEST_SkipToNextSpecialByte(const char* start,
+ const char* limit) {
+ return SkipToNextSpecialByte(start, limit);
+}
+
+// Helper routine to encode "s" and append to "*dest", escaping special
+// characters.
+inline static void EncodeStringFragment(std::string* dest, Slice s) {
+ const char* p = s.data();
+ const char* limit = p + s.size();
+ const char* copy_start = p;
+ while (true) {
+ p = SkipToNextSpecialByte(p, limit);
+ if (p >= limit) break; // No more special characters that need escaping
+ char c = *(p++);
+ assert(IsSpecialByte(c));
+ if (c == kEscape1) {
+ AppendBytes(dest, copy_start, p - copy_start - 1);
+ dest->push_back(kEscape1);
+ dest->push_back(kNullCharacter);
+ copy_start = p;
+ } else {
+ assert(c == kEscape2);
+ AppendBytes(dest, copy_start, p - copy_start - 1);
+ dest->push_back(kEscape2);
+ dest->push_back(kFFCharacter);
+ copy_start = p;
+ }
+ }
+ if (p > copy_start) {
+ AppendBytes(dest, copy_start, p - copy_start);
+ }
+}
+
+void OrderedCode::WriteString(std::string* dest, Slice s) {
+ EncodeStringFragment(dest, s);
+ AppendBytes(dest, kEscape1_Separator, 2);
+}
+
+// Return number of bytes needed to encode the non-length portion
+// of val in ordered coding. Returns number in range [0,8].
+static inline unsigned int OrderedNumLength(uint64_t val) {
+ const int lg = Bits::Log2Floor64(val); // -1 if val==0
+ return static_cast<unsigned int>(lg + 1 + 7) / 8;
+}
+
+// Append n bytes from src to *dst.
+// REQUIRES: n <= 9
+// REQUIRES: src[0..8] are readable bytes (even if n is smaller)
+//
+// If we use string::append() instead of this routine, it increases the
+// runtime of WriteNumIncreasing from ~9ns to ~13ns.
+static inline void AppendUpto9(std::string* dst, const char* src,
+ unsigned int n) {
+ dst->append(src, 9); // Fixed-length append
+ const size_t extra = 9 - n; // How many extra bytes we added
+ dst->erase(dst->size() - extra, extra);
+}
+
+void OrderedCode::WriteNumIncreasing(std::string* dest, uint64_t val) {
+ // Values are encoded with a single byte length prefix, followed
+ // by the actual value in big-endian format with leading 0 bytes
+ // dropped.
+
+ // 8 bytes for value plus one byte for length. In addition, we have
+ // 8 extra bytes at the end so that we can have a fixed-length append
+ // call on *dest.
+ char buf[17];
+
+ UNALIGNED_STORE64(buf + 1, absl::ghtonll(val)); // buf[0] may be needed for length
+ const unsigned int length = OrderedNumLength(val);
+ char* start = buf + 9 - length - 1;
+ *start = length;
+ AppendUpto9(dest, start, length + 1);
+}
+
+inline static void WriteInfinityInternal(std::string* dest) {
+ // Make an array so that we can just do one string operation for performance
+ static const char buf[2] = {kEscape2, kInfinity};
+ dest->append(buf, 2);
+}
+
+void OrderedCode::WriteInfinity(std::string* dest) {
+ WriteInfinityInternal(dest);
+}
+
+void OrderedCode::WriteTrailingString(std::string* dest, Slice str) {
+ dest->append(str.data(), str.size());
+}
+
+// Parse the encoding of a string previously encoded with or without
+// inversion. If parse succeeds, return true, consume encoding from
+// "*src", and if result != NULL append the decoded string to "*result".
+// Otherwise, return false and leave both undefined.
+inline static bool ReadStringInternal(Slice* src, std::string* result) {
+ const char* start = src->data();
+ const char* string_limit = src->data() + src->size();
+
+ // We only scan up to "limit-2" since a valid string must end with
+ // a two character terminator: 'kEscape1 kSeparator'
+ const char* limit = string_limit - 1;
+ const char* copy_start = start;
+ while (true) {
+ start = SkipToNextSpecialByte(start, limit);
+ if (start >= limit) break; // No terminator sequence found
+ const char c = *(start++);
+ // If inversion is required, instead of inverting 'c', we invert the
+ // character constants to which 'c' is compared. We get the same
+ // behavior but save the runtime cost of inverting 'c'.
+ assert(IsSpecialByte(c));
+ if (c == kEscape1) {
+ if (result) {
+ AppendBytes(result, copy_start, start - copy_start - 1);
+ }
+ // kEscape1 kSeparator ends component
+ // kEscape1 kNullCharacter represents '\0'
+ const char next = *(start++);
+ if (next == kSeparator) {
+ src->remove_prefix(start - src->data());
+ return true;
+ } else if (next == kNullCharacter) {
+ if (result) {
+ *result += '\0';
+ }
+ } else {
+ return false;
+ }
+ copy_start = start;
+ } else {
+ assert(c == kEscape2);
+ if (result) {
+ AppendBytes(result, copy_start, start - copy_start - 1);
+ }
+ // kEscape2 kFFCharacter represents '\xff'
+ // kEscape2 kInfinity is an error
+ const char next = *(start++);
+ if (next == kFFCharacter) {
+ if (result) {
+ *result += '\xff';
+ }
+ } else {
+ return false;
+ }
+ copy_start = start;
+ }
+ }
+ return false;
+}
+
+bool OrderedCode::ReadString(Slice* src, std::string* result) {
+ return ReadStringInternal(src, result);
+}
+
+bool OrderedCode::ReadNumIncreasing(Slice* src, uint64_t* result) {
+ if (src->empty()) {
+ return false; // Not enough bytes
+ }
+
+ // Decode length byte
+ const int len = static_cast<unsigned char>((*src)[0]);
+
+ // If len > 0 and src is longer than 1, the first byte of "payload"
+ // must be non-zero (otherwise the encoding is not minimal).
+ // In opt mode, we don't enforce that encodings must be minimal.
+ assert(0 == len || src->size() == 1 || (*src)[1] != '\0');
+
+ if (len + 1 > src->size() || len > 8) {
+ return false; // Not enough bytes or too many bytes
+ }
+
+ if (result) {
+ uint64_t tmp = 0;
+ for (int i = 0; i < len; i++) {
+ tmp <<= 8;
+ tmp |= static_cast<unsigned char>((*src)[1 + i]);
+ }
+ *result = tmp;
+ }
+ src->remove_prefix(len + 1);
+ return true;
+}
+
+inline static bool ReadInfinityInternal(Slice* src) {
+ if (src->size() >= 2 && ((*src)[0] == kEscape2) && ((*src)[1] == kInfinity)) {
+ src->remove_prefix(2);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool OrderedCode::ReadInfinity(Slice* src) { return ReadInfinityInternal(src); }
+
+inline static bool ReadStringOrInfinityInternal(Slice* src, std::string* result,
+ bool* inf) {
+ if (ReadInfinityInternal(src)) {
+ if (inf) *inf = true;
+ return true;
+ }
+
+ // We don't use ReadStringInternal here because that would inline
+ // the whole encoded string parsing code here. Depending on INVERT, only
+ // one of the following two calls will be generated at compile time.
+ bool success = OrderedCode::ReadString(src, result);
+ if (success) {
+ if (inf) *inf = false;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool OrderedCode::ReadStringOrInfinity(Slice* src, std::string* result,
+ bool* inf) {
+ return ReadStringOrInfinityInternal(src, result, inf);
+}
+
+bool OrderedCode::ReadTrailingString(Slice* src, std::string* result) {
+ if (result) result->assign(src->data(), src->size());
+ src->remove_prefix(src->size());
+ return true;
+}
+
+void OrderedCode::TEST_Corrupt(std::string* str, int k) {
+ int seen_seps = 0;
+ for (int i = 0; i < str->size() - 1; i++) {
+ if ((*str)[i] == kEscape1 && (*str)[i + 1] == kSeparator) {
+ seen_seps++;
+ if (seen_seps == k) {
+ (*str)[i + 1] = kSeparator + 1;
+ return;
+ }
+ }
+ }
+}
+
+// Signed number encoding/decoding /////////////////////////////////////
+//
+// The format is as follows:
+//
+// The first bit (the most significant bit of the first byte)
+// represents the sign, 0 if the number is negative and
+// 1 if the number is >= 0.
+//
+// Any unbroken sequence of successive bits with the same value as the sign
+// bit, up to 9 (the 8th and 9th are the most significant bits of the next
+// byte), are size bits that count the number of bytes after the first byte.
+// That is, the total length is between 1 and 10 bytes.
+//
+// The value occupies the bits after the sign bit and the "size bits"
+// till the end of the string, in network byte order. If the number
+// is negative, the bits are in 2-complement.
+//
+//
+// Example 1: number 0x424242 -> 4 byte big-endian hex string 0xf0424242:
+//
+// +---------------+---------------+---------------+---------------+
+// 1 1 1 1 0 0 0 0 0 1 0 0 0 0 1 0 0 1 0 0 0 1 0 0 0 1 0 0 0 0 1 0
+// +---------------+---------------+---------------+---------------+
+// ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
+// | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
+// | | | | payload: the remaining bits after the sign and size bits
+// | | | | and the delimiter bit, the value is 0x424242
+// | | | |
+// | size bits: 3 successive bits with the same value as the sign bit
+// | (followed by a delimiter bit with the opposite value)
+// | mean that there are 3 bytes after the first byte, 4 total
+// |
+// sign bit: 1 means that the number is non-negative
+//
+// Example 2: negative number -0x800 -> 2 byte big-endian hex string 0x3800:
+//
+// +---------------+---------------+
+// 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0
+// +---------------+---------------+
+// ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
+// | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
+// | | payload: the remaining bits after the sign and size bits and the
+// | | delimiter bit, 2-complement because of the negative sign,
+// | | value is ~0x7ff, represents the value -0x800
+// | |
+// | size bits: 1 bit with the same value as the sign bit
+// | (followed by a delimiter bit with the opposite value)
+// | means that there is 1 byte after the first byte, 2 total
+// |
+// sign bit: 0 means that the number is negative
+//
+//
+// Compared with the simpler unsigned format used for uint64_t numbers,
+// this format is more compact for small numbers, namely one byte encodes
+// numbers in the range [-64,64), two bytes cover the range [-2^13,2^13), etc.
+// In general, n bytes encode numbers in the range [-2^(n*7-1),2^(n*7-1)).
+// (The cross-over point for compactness of representation is 8 bytes,
+// where this format only covers the range [-2^55,2^55),
+// whereas an encoding with sign bit and length in the first byte and
+// payload in all following bytes would cover [-2^56,2^56).)
+
+static const int kMaxSigned64Length = 10;
+
+// This array maps encoding length to header bits in the first two bytes.
+static const char kLengthToHeaderBits[1 + kMaxSigned64Length][2] = {
+ {0, 0}, {'\x80', 0}, {'\xc0', 0}, {'\xe0', 0},
+ {'\xf0', 0}, {'\xf8', 0}, {'\xfc', 0}, {'\xfe', 0},
+ {'\xff', 0}, {'\xff', '\x80'}, {'\xff', '\xc0'}};
+
+// This array maps encoding lengths to the header bits that overlap with
+// the payload and need fixing when reading.
+static const uint64_t kLengthToMask[1 + kMaxSigned64Length] = {
+ 0ULL,
+ 0x80ULL,
+ 0xc000ULL,
+ 0xe00000ULL,
+ 0xf0000000ULL,
+ 0xf800000000ULL,
+ 0xfc0000000000ULL,
+ 0xfe000000000000ULL,
+ 0xff00000000000000ULL,
+ 0x8000000000000000ULL,
+ 0ULL};
+
+// This array maps the number of bits in a number to the encoding
+// length produced by WriteSignedNumIncreasing.
+// For positive numbers, the number of bits is 1 plus the most significant
+// bit position (the highest bit position in a positive int64_t is 63).
+// For a negative number n, we count the bits in ~n.
+// That is, length = kBitsToLength[Bits::Log2Floor64(n < 0 ? ~n : n) + 1].
+static const int8_t kBitsToLength[1 + 63] = {
+ 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4,
+ 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 7,
+ 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10};
+
+// Calculates the encoding length in bytes of the signed number n.
+static inline int SignedEncodingLength(int64_t n) {
+ return kBitsToLength[Bits::Log2Floor64(n < 0 ? ~n : n) + 1];
+}
+
+// Slightly faster version for n > 0.
+static inline int SignedEncodingLengthPositive(int64_t n) {
+ return kBitsToLength[Bits::Log2FloorNonZero64(n) + 1];
+}
+
+void OrderedCode::WriteSignedNumIncreasing(std::string* dest, int64_t val) {
+ const uint64_t x = val < 0 ? ~val : val;
+ if (x < 64) { // fast path for encoding length == 1
+ *dest += kLengthToHeaderBits[1][0] ^ val;
+ return;
+ }
+ // buf = val in network byte order, sign extended to 10 bytes
+ const char sign_byte = val < 0 ? '\xff' : '\0';
+ char buf[10] = {
+ sign_byte, sign_byte,
+ };
+ UNALIGNED_STORE64(buf + 2, absl::ghtonll(val));
+
+ static_assert(sizeof(buf) == kMaxSigned64Length, "max length size mismatch");
+ const int len = SignedEncodingLengthPositive(x);
+ assert(len >= 2);
+ char* const begin = buf + sizeof(buf) - len;
+ begin[0] ^= kLengthToHeaderBits[len][0];
+ begin[1] ^= kLengthToHeaderBits[len][1]; // ok because len >= 2
+ dest->append(begin, len);
+}
+
+bool OrderedCode::ReadSignedNumIncreasing(Slice* src, int64_t* result) {
+ if (src->empty()) return false;
+ const uint64_t xor_mask = (!((*src)[0] & 0x80)) ? ~0ULL : 0ULL;
+ const unsigned char first_byte = (*src)[0] ^ (xor_mask & 0xff);
+
+ // now calculate and test length, and set x to raw (unmasked) result
+ int len;
+ uint64_t x;
+ if (first_byte != 0xff) {
+ len = 7 - Bits::Log2FloorNonZero(first_byte ^ 0xff);
+ if (src->size() < len) return false;
+ x = xor_mask; // sign extend using xor_mask
+ for (int i = 0; i < len; ++i)
+ x = (x << 8) | static_cast<unsigned char>((*src)[i]);
+ } else {
+ len = 8;
+ if (src->size() < len) return false;
+ const unsigned char second_byte = (*src)[1] ^ (xor_mask & 0xff);
+ if (second_byte >= 0x80) {
+ if (second_byte < 0xc0) {
+ len = 9;
+ } else {
+ const unsigned char third_byte = (*src)[2] ^ (xor_mask & 0xff);
+ if (second_byte == 0xc0 && third_byte < 0x80) {
+ len = 10;
+ } else {
+ return false; // either len > 10 or len == 10 and #bits > 63
+ }
+ }
+ if (src->size() < len) return false;
+ }
+ x = absl::gntohll(UNALIGNED_LOAD64(src->data() + len - 8));
+ }
+
+ x ^= kLengthToMask[len]; // remove spurious header bits
+
+ assert(len == SignedEncodingLength(x));
+
+ if (result) *result = x;
+ src->remove_prefix(len);
+ return true;
+}
+
+} // namespace Firestore
+
diff --git a/Firestore/Port/ordered_code.h b/Firestore/Port/ordered_code.h
new file mode 100644
index 0000000..7c390a5
--- /dev/null
+++ b/Firestore/Port/ordered_code.h
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// This module provides routines for encoding a sequence of typed
+// entities into a string. The resulting strings can be
+// lexicographically compared to yield the same comparison value that
+// would have been generated if the encoded items had been compared
+// one by one according to their type.
+//
+// More precisely, suppose:
+// 1. string A is generated by encoding the sequence of items [A_1..A_n]
+// 2. string B is generated by encoding the sequence of items [B_1..B_n]
+// 3. The types match; i.e., for all i: A_i was encoded using
+// the same routine as B_i
+// Then:
+// Comparing A vs. B lexicographically is the same as comparing
+// the vectors [A_1..A_n] and [B_1..B_n] lexicographically.
+//
+// Furthermore, if n < m, the encoding of [A_1..A_n] is a strict prefix of
+// [A_1..A_m] (unless m = n+1 and A_m is the empty string encoded with
+// WriteTrailingString, in which case the encodings are equal).
+//
+// This module is often useful when generating multi-part sstable
+// keys that have to be ordered in a particular fashion.
+
+#ifndef IPHONE_FIRESTORE_PORT_ORDERED_CODE_H_
+#define IPHONE_FIRESTORE_PORT_ORDERED_CODE_H_
+
+#include <string>
+
+namespace leveldb {
+class Slice;
+}
+
+namespace Firestore {
+
+class OrderedCode {
+ public:
+ // -------------------------------------------------------------------
+ // Encoding routines: each one of the following routines append
+ // one item to "*dest". The Write(Signed)NumIncreasing() and
+ // Write(Signed)NumDecreasing() routines differ in whether the resulting
+ // encoding is ordered by increasing number or decreasing number.
+ // Similarly, WriteString() and WriteStringDecreasing() differ in whether
+ // the resulting encoding is ordered by the original string in
+ // lexicographically increasing or decreasing order. WriteString()
+ // is not called WriteStringIncreasing() for convenience and backward
+ // compatibility.
+
+ static void WriteString(std::string* dest, leveldb::Slice str);
+ static void WriteNumIncreasing(std::string* dest, uint64_t num);
+ static void WriteSignedNumIncreasing(std::string* dest, int64_t num);
+
+ // Creates an encoding for the "infinite string", a value considered to
+ // be lexicographically after any real string. Note that in the case of
+ // WriteInfinityDecreasing(), this would come before any real string as
+ // the ordering puts lexicographically greater values first.
+ static void WriteInfinity(std::string* dest);
+
+ // Special string append that can only be used at the tail end of
+ // an encoded string -- blindly appends "str" to "*dest".
+ static void WriteTrailingString(std::string* dest, leveldb::Slice str);
+
+ // -------------------------------------------------------------------
+ // Decoding routines: these extract an item earlier encoded using
+ // the corresponding WriteXXX() routines above. The item is read
+ // from "*src"; "*src" is modified to point past the decoded item;
+ // and if "result" is non-NULL, "*result" is modified to contain the
+ // result. In case of string result, the decoded string is appended to
+ // "*result". Returns true if the next item was read successfully, false
+ // otherwise.
+
+ static bool ReadString(leveldb::Slice* src, std::string* result);
+ static bool ReadNumIncreasing(leveldb::Slice* src, uint64_t* result);
+ static bool ReadSignedNumIncreasing(leveldb::Slice* src, int64_t* result);
+
+ static bool ReadInfinity(leveldb::Slice* src);
+ static bool ReadTrailingString(leveldb::Slice* src, std::string* result);
+
+ // REQUIRES: next item was encoded by WriteInfinity() or WriteString()
+ static bool ReadStringOrInfinity(leveldb::Slice* src, std::string* result, bool* inf);
+
+ // Helper for testing: corrupt "*str" by changing the kth item separator
+ // in the string.
+ static void TEST_Corrupt(std::string* str, int k);
+
+ // Helper for testing.
+ // SkipToNextSpecialByte is an internal routine defined in the .cc file
+ // with the following semantics. Return a pointer to the first byte
+ // in the range "[start..limit)" whose value is 0 or 255. If no such
+ // byte exists in the range, returns "limit".
+ static const char* TEST_SkipToNextSpecialByte(const char* start, const char* limit);
+
+ // Not an instantiable class, but the class exists to make it easy to
+ // use with a single using statement.
+ OrderedCode() = delete;
+ OrderedCode(const OrderedCode&) = delete;
+ OrderedCode& operator=(const OrderedCode&) = delete;
+};
+
+} // namespace Firestore
+
+#endif // IPHONE_FIRESTORE_PORT_ORDERED_CODE_H_
diff --git a/Firestore/Port/ordered_code_test.cc b/Firestore/Port/ordered_code_test.cc
new file mode 100644
index 0000000..21b0bd1
--- /dev/null
+++ b/Firestore/Port/ordered_code_test.cc
@@ -0,0 +1,528 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ordered_code.h"
+
+// #include <float.h>
+// #include <stddef.h>
+#include <iostream>
+#include <limits>
+
+#include "base/logging.h"
+#include "testing/base/public/gunit.h"
+#include <leveldb/db.h>
+#include "util/random/acmrandom.h"
+
+using Firestore::OrderedCode;
+using leveldb::Slice;
+
+// Make Slices writeable to ostream, making all the CHECKs happy below.
+namespace {
+void WritePadding(std::ostream& o, size_t pad) {
+ char fill_buf[32];
+ memset(fill_buf, o.fill(), sizeof(fill_buf));
+ while (pad) {
+ size_t n = std::min(pad, sizeof(fill_buf));
+ o.write(fill_buf, n);
+ pad -= n;
+ }
+}
+} // namespace
+
+namespace leveldb {
+
+std::ostream& operator<<(std::ostream& o, const Slice slice) {
+ std::ostream::sentry sentry(o);
+ if (sentry) {
+ size_t lpad = 0;
+ size_t rpad = 0;
+ if (o.width() > slice.size()) {
+ size_t pad = o.width() - slice.size();
+ if ((o.flags() & o.adjustfield) == o.left) {
+ rpad = pad;
+ } else {
+ lpad = pad;
+ }
+ }
+ if (lpad) WritePadding(o, lpad);
+ o.write(slice.data(), slice.size());
+ if (rpad) WritePadding(o, rpad);
+ o.width(0);
+ }
+ return o;
+}
+
+} // namespace leveldb
+
+static std::string RandomString(ACMRandom* rnd, int len) {
+ std::string x;
+ for (int i = 0; i < len; i++) {
+ x += rnd->Uniform(256);
+ }
+ return x;
+}
+
+// ---------------------------------------------------------------------
+// Utility template functions (they help templatize the tests below)
+
+// Read/WriteIncreasing are defined for string, uint64_t, int64_t below.
+template <typename T>
+static void OCWriteIncreasing(std::string* dest, const T& val);
+template <typename T>
+static bool OCReadIncreasing(Slice* src, T* result);
+
+// Read/WriteIncreasing<std::string>
+template <>
+void OCWriteIncreasing<std::string>(std::string* dest, const std::string& val) {
+ OrderedCode::WriteString(dest, val);
+}
+template <>
+bool OCReadIncreasing<std::string>(Slice* src, std::string* result) {
+ return OrderedCode::ReadString(src, result);
+}
+
+// Read/WriteIncreasing<uint64_t>
+template <>
+void OCWriteIncreasing<uint64_t>(std::string* dest, const uint64_t& val) {
+ OrderedCode::WriteNumIncreasing(dest, val);
+}
+template <>
+bool OCReadIncreasing<uint64_t>(Slice* src, uint64_t* result) {
+ return OrderedCode::ReadNumIncreasing(src, result);
+}
+
+enum Direction { INCREASING = 0 };
+
+// Read/WriteIncreasing<int64_t>
+template <>
+void OCWriteIncreasing<int64_t>(std::string* dest, const int64_t& val) {
+ OrderedCode::WriteSignedNumIncreasing(dest, val);
+}
+template <>
+bool OCReadIncreasing<int64_t>(Slice* src, int64_t* result) {
+ return OrderedCode::ReadSignedNumIncreasing(src, result);
+}
+
+template <typename T>
+std::string OCWrite(T val, Direction direction) {
+ std::string result;
+ OCWriteIncreasing<T>(&result, val);
+ return result;
+}
+
+template <typename T>
+void OCWriteToString(std::string* result, T val, Direction direction) {
+ OCWriteIncreasing<T>(result, val);
+}
+
+template <typename T>
+bool OCRead(Slice* s, T* val, Direction direction) {
+ return OCReadIncreasing<T>(s, val);
+}
+
+// ---------------------------------------------------------------------
+// Numbers
+
+template <typename T>
+static T TestRead(Direction d, const std::string& a) {
+ // gracefully reject any proper prefix of an encoding
+ for (int i = 0; i < a.size() - 1; ++i) {
+ Slice s(a.data(), i);
+ CHECK(!OCRead<T>(&s, NULL, d));
+ CHECK_EQ(s, a.substr(0, i));
+ }
+
+ Slice s(a);
+ T v;
+ CHECK(OCRead<T>(&s, &v, d));
+ CHECK(s.empty());
+ return v;
+}
+
+template <typename T>
+static void TestWriteRead(Direction d, T expected) {
+ EXPECT_EQ(expected, TestRead<T>(d, OCWrite<T>(expected, d)));
+}
+
+// Verifies that the second Write* call appends a non-empty std::string to its
+// output.
+template <typename T, typename U>
+static void TestWriteAppends(Direction d, T first, U second) {
+ std::string encoded;
+ OCWriteToString<T>(&encoded, first, d);
+ std::string encoded_first_only = encoded;
+ OCWriteToString<U>(&encoded, second, d);
+ EXPECT_NE(encoded, encoded_first_only);
+ EXPECT_TRUE(Slice(encoded).starts_with(encoded_first_only));
+}
+
+template <typename T>
+static void TestNumbers(T multiplier) {
+ for (int j = 0; j < 2; ++j) {
+ const Direction d = static_cast<Direction>(j);
+
+ // first test powers of 2 (and nearby numbers)
+ for (T x = std::numeric_limits<T>().max(); x != 0; x /= 2) {
+ TestWriteRead(d, multiplier * (x - 1));
+ TestWriteRead(d, multiplier * x);
+ if (x != std::numeric_limits<T>::max()) {
+ TestWriteRead(d, multiplier * (x + 1));
+ } else if (multiplier < 0 && multiplier == -1) {
+ TestWriteRead(d, -x - 1);
+ }
+ }
+
+ ACMRandom rnd(301);
+ for (int bits = 1; bits <= std::numeric_limits<T>().digits; ++bits) {
+ // test random non-negative numbers with given number of significant bits
+ const uint64_t mask = (~0ULL) >> (64 - bits);
+ for (int i = 0; i < 1000; i++) {
+ T x = rnd.Next64() & mask;
+ TestWriteRead(d, multiplier * x);
+ T y = rnd.Next64() & mask;
+ TestWriteAppends(d, multiplier * x, multiplier * y);
+ }
+ }
+ }
+}
+
+// Return true iff 'a' is "before" 'b' according to 'direction'
+static bool CompareStrings(const std::string& a, const std::string& b,
+ Direction d) {
+ return (INCREASING == d) ? (a < b) : (b < a);
+}
+
+template <typename T>
+static void TestNumberOrdering() {
+ const Direction d = INCREASING;
+
+ // first the negative numbers (if T is signed, otherwise no-op)
+ std::string laststr = OCWrite<T>(std::numeric_limits<T>().min(), d);
+ for (T num = std::numeric_limits<T>().min() / 2; num != 0; num /= 2) {
+ std::string strminus1 = OCWrite<T>(num - 1, d);
+ std::string str = OCWrite<T>(num, d);
+ std::string strplus1 = OCWrite<T>(num + 1, d);
+
+ CHECK(CompareStrings(strminus1, str, d));
+ CHECK(CompareStrings(str, strplus1, d));
+
+ // Compare 'str' with 'laststr'. When we approach 0, 'laststr' is
+ // not necessarily before 'strminus1'.
+ CHECK(CompareStrings(laststr, str, d));
+ laststr = str;
+ }
+
+ // then the positive numbers
+ laststr = OCWrite<T>(0, d);
+ T num = 1;
+ while (num < std::numeric_limits<T>().max() / 2) {
+ num *= 2;
+ std::string strminus1 = OCWrite<T>(num - 1, d);
+ std::string str = OCWrite<T>(num, d);
+ std::string strplus1 = OCWrite<T>(num + 1, d);
+
+ CHECK(CompareStrings(strminus1, str, d));
+ CHECK(CompareStrings(str, strplus1, d));
+
+ // Compare 'str' with 'laststr'.
+ CHECK(CompareStrings(laststr, str, d));
+ laststr = str;
+ }
+}
+
+// Helper routine for testing TEST_SkipToNextSpecialByte
+static int FindSpecial(const std::string& x) {
+ const char* p = x.data();
+ const char* limit = p + x.size();
+ const char* result = OrderedCode::TEST_SkipToNextSpecialByte(p, limit);
+ return result - p;
+}
+
+TEST(OrderedCode, SkipToNextSpecialByte) {
+ for (int len = 0; len < 256; len++) {
+ ACMRandom rnd(301);
+ std::string x;
+ while (x.size() < len) {
+ char c = 1 + rnd.Uniform(254);
+ ASSERT_NE(c, 0);
+ ASSERT_NE(c, 255);
+ x += c; // No 0 bytes, no 255 bytes
+ }
+ EXPECT_EQ(FindSpecial(x), x.size());
+ for (int special_pos = 0; special_pos < len; special_pos++) {
+ for (int special_test = 0; special_test < 2; special_test++) {
+ const char special_byte = (special_test == 0) ? 0 : 255;
+ std::string y = x;
+ y[special_pos] = special_byte;
+ EXPECT_EQ(FindSpecial(y), special_pos);
+ if (special_pos < 16) {
+ // Add some special bytes after the one at special_pos to make sure
+ // we still return the earliest special byte in the string
+ for (int rest = special_pos + 1; rest < len; rest++) {
+ if (rnd.OneIn(3)) {
+ y[rest] = rnd.OneIn(2) ? 0 : 255;
+ EXPECT_EQ(FindSpecial(y), special_pos);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+TEST(OrderedCode, ExhaustiveFindSpecial) {
+ char buf[16];
+ char* limit = buf + sizeof(buf);
+ int count = 0;
+ for (int start_offset = 0; start_offset <= 5; start_offset += 5) {
+ // We test exhaustively with all combinations of 3 bytes starting
+ // at offset 0 and offset 5 (so as to test with the bytes at both
+ // ends of a 64-bit word).
+ for (char& c : buf) {
+ c = 'a'; // Not a special byte
+ }
+ for (int b0 = 0; b0 < 256; b0++) {
+ for (int b1 = 0; b1 < 256; b1++) {
+ for (int b2 = 0; b2 < 256; b2++) {
+ buf[start_offset + 0] = b0;
+ buf[start_offset + 1] = b1;
+ buf[start_offset + 2] = b2;
+ char* expected;
+ if (b0 == 0 || b0 == 255) {
+ expected = &buf[start_offset];
+ } else if (b1 == 0 || b1 == 255) {
+ expected = &buf[start_offset + 1];
+ } else if (b2 == 0 || b2 == 255) {
+ expected = &buf[start_offset + 2];
+ } else {
+ expected = limit;
+ }
+ count++;
+ EXPECT_EQ(expected,
+ OrderedCode::TEST_SkipToNextSpecialByte(buf, limit));
+ }
+ }
+ }
+ }
+ EXPECT_EQ(count, 256 * 256 * 256 * 2);
+}
+
+TEST(Uint64, EncodeDecode) { TestNumbers<uint64_t>(1); }
+
+TEST(Uint64, Ordering) { TestNumberOrdering<uint64_t>(); }
+
+TEST(Int64, EncodeDecode) {
+ TestNumbers<int64_t>(1);
+ TestNumbers<int64_t>(-1);
+}
+
+TEST(Int64, Ordering) { TestNumberOrdering<int64_t>(); }
+
+// Returns the bitwise complement of s.
+static inline std::string StrNot(const std::string& s) {
+ std::string result;
+ for (const char c : s) result.push_back(~c);
+ return result;
+}
+
+template <typename T>
+static void TestInvalidEncoding(Direction d, const std::string& s) {
+ Slice p(s);
+ EXPECT_FALSE(OCRead<T>(&p, static_cast<T*>(NULL), d));
+ EXPECT_EQ(s, p);
+}
+
+TEST(OrderedCodeInvalidEncodingsTest, Overflow) {
+ // 1U << 64, increasing
+ const std::string k2xx64U = "\x09\x01" + std::string(8, 0);
+ TestInvalidEncoding<uint64_t>(INCREASING, k2xx64U);
+
+ // 1 << 63 and ~(1 << 63), increasing
+ const std::string k2xx63 = "\xff\xc0\x80" + std::string(7, 0);
+ TestInvalidEncoding<int64_t>(INCREASING, k2xx63);
+ TestInvalidEncoding<int64_t>(INCREASING, StrNot(k2xx63));
+}
+
+TEST(OrderedCodeInvalidEncodingsTest, NonCanonical) {
+ // Test DCHECK failures of "ambiguous"/"non-canonical" encodings.
+ // These are non-minimal (but otherwise "valid") encodings that
+ // differ from the minimal encoding chosen by OrderedCode::WriteXXX
+ // and thus should be avoided to not mess up the string ordering of
+ // encodings.
+
+ ACMRandom rnd(301);
+
+ for (int n = 2; n <= 9; ++n) {
+ // The zero in non_minimal[1] is "redundant".
+ std::string non_minimal =
+ std::string(1, n - 1) + std::string(1, 0) + RandomString(&rnd, n - 2);
+ EXPECT_EQ(n, non_minimal.length());
+
+ EXPECT_NE(OCWrite<uint64_t>(0, INCREASING), non_minimal);
+ if (DEBUG_MODE) {
+ Slice s(non_minimal);
+ EXPECT_DEATH_IF_SUPPORTED(OrderedCode::ReadNumIncreasing(&s, NULL),
+ "ssertion failed");
+ } else {
+ TestRead<uint64_t>(INCREASING, non_minimal);
+ }
+ }
+
+ for (int n = 2; n <= 10; ++n) {
+ // Header with 1 sign bit and n-1 size bits.
+ std::string header =
+ std::string(n / 8, 0xff) + std::string(1, 0xff << (8 - (n % 8)));
+ // There are more than 7 zero bits between header bits and "payload".
+ std::string non_minimal =
+ header + std::string(1, rnd.Uniform(256) & ~*header.rbegin()) +
+ RandomString(&rnd, n - header.length() - 1);
+ EXPECT_EQ(n, non_minimal.length());
+
+ EXPECT_NE(OCWrite<int64_t>(0, INCREASING), non_minimal);
+ if (DEBUG_MODE) {
+ Slice s(non_minimal);
+ EXPECT_DEATH_IF_SUPPORTED(OrderedCode::ReadSignedNumIncreasing(&s, NULL),
+ "ssertion failed")
+ << n;
+ s = non_minimal;
+ } else {
+ TestRead<int64_t>(INCREASING, non_minimal);
+ }
+ }
+}
+
+// ---------------------------------------------------------------------
+// Strings
+
+TEST(String, Infinity) {
+ const std::string value("\xff\xff foo");
+ bool is_inf;
+ std::string encoding, parsed;
+ Slice s;
+
+ // Check encoding/decoding of "infinity" for ascending order
+ encoding.clear();
+ OrderedCode::WriteInfinity(&encoding);
+ encoding.push_back('a');
+ s = encoding;
+ EXPECT_TRUE(OrderedCode::ReadInfinity(&s));
+ EXPECT_EQ(1, s.size());
+ s = encoding;
+ is_inf = false;
+ EXPECT_TRUE(OrderedCode::ReadStringOrInfinity(&s, NULL, &is_inf));
+ EXPECT_EQ(1, s.size());
+ EXPECT_TRUE(is_inf);
+
+ // Check ReadStringOrInfinity() can parse ordinary strings
+ encoding.clear();
+ OrderedCode::WriteString(&encoding, value);
+ encoding.push_back('a');
+ s = encoding;
+ is_inf = false;
+ parsed.clear();
+ EXPECT_TRUE(OrderedCode::ReadStringOrInfinity(&s, &parsed, &is_inf));
+ EXPECT_EQ(1, s.size());
+ EXPECT_FALSE(is_inf);
+ EXPECT_EQ(value, parsed);
+}
+
+TEST(String, EncodeDecode) {
+ ACMRandom rnd(301);
+ for (int i = 0; i < 2; ++i) {
+ const Direction d = static_cast<Direction>(i);
+
+ for (int len = 0; len < 256; len++) {
+ const std::string a = RandomString(&rnd, len);
+ TestWriteRead(d, a);
+ for (int len2 = 0; len2 < 64; len2++) {
+ const std::string b = RandomString(&rnd, len2);
+
+ TestWriteAppends(d, a, b);
+
+ std::string out;
+ OCWriteToString<std::string>(&out, a, d);
+ OCWriteToString<std::string>(&out, b, d);
+
+ std::string a2, b2, dummy;
+ Slice s = out;
+ Slice s2 = out;
+ CHECK(OCRead<std::string>(&s, &a2, d));
+ CHECK(OCRead<std::string>(&s2, NULL, d));
+ CHECK_EQ(s, s2);
+
+ CHECK(OCRead<std::string>(&s, &b2, d));
+ CHECK(OCRead<std::string>(&s2, NULL, d));
+ CHECK_EQ(s, s2);
+
+ CHECK(!OCRead<std::string>(&s, &dummy, d));
+ CHECK(!OCRead<std::string>(&s2, NULL, d));
+ CHECK_EQ(a, a2);
+ CHECK_EQ(b, b2);
+ CHECK(s.empty());
+ CHECK(s2.empty());
+ }
+ }
+ }
+}
+
+// 'str' is a static C-style string that may contain '\0'
+#define STATIC_STR(str) Slice((str), sizeof(str) - 1)
+
+static std::string EncodeStringIncreasing(Slice value) {
+ std::string encoded;
+ OrderedCode::WriteString(&encoded, value);
+ return encoded;
+}
+
+TEST(String, Increasing) {
+ // Here are a series of strings in non-decreasing order, including
+ // consecutive strings such that the second one is equal to, a proper
+ // prefix of, or has the same length as the first one. Most also contain
+ // the special escaping characters '\x00' and '\xff'.
+ ASSERT_EQ(EncodeStringIncreasing(STATIC_STR("")),
+ EncodeStringIncreasing(STATIC_STR("")));
+
+ ASSERT_LT(EncodeStringIncreasing(STATIC_STR("")),
+ EncodeStringIncreasing(STATIC_STR("\x00")));
+
+ ASSERT_EQ(EncodeStringIncreasing(STATIC_STR("\x00")),
+ EncodeStringIncreasing(STATIC_STR("\x00")));
+
+ ASSERT_LT(EncodeStringIncreasing(STATIC_STR("\x00")),
+ EncodeStringIncreasing(STATIC_STR("\x01")));
+
+ ASSERT_LT(EncodeStringIncreasing(STATIC_STR("\x01")),
+ EncodeStringIncreasing(STATIC_STR("a")));
+
+ ASSERT_EQ(EncodeStringIncreasing(STATIC_STR("a")),
+ EncodeStringIncreasing(STATIC_STR("a")));
+
+ ASSERT_LT(EncodeStringIncreasing(STATIC_STR("a")),
+ EncodeStringIncreasing(STATIC_STR("aa")));
+
+ ASSERT_LT(EncodeStringIncreasing(STATIC_STR("aa")),
+ EncodeStringIncreasing(STATIC_STR("\xff")));
+
+ ASSERT_LT(EncodeStringIncreasing(STATIC_STR("\xff")),
+ EncodeStringIncreasing(STATIC_STR("\xff\x00")));
+
+ ASSERT_LT(EncodeStringIncreasing(STATIC_STR("\xff\x00")),
+ EncodeStringIncreasing(STATIC_STR("\xff\x01")));
+
+ std::string infinity;
+ OrderedCode::WriteInfinity(&infinity);
+ ASSERT_LT(EncodeStringIncreasing(std::string(1 << 20, '\xff')), infinity);
+}
diff --git a/Firestore/Port/string_util.cc b/Firestore/Port/string_util.cc
new file mode 100644
index 0000000..5e87fff
--- /dev/null
+++ b/Firestore/Port/string_util.cc
@@ -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.
+ */
+
+#include "string_util.h"
+
+#include <leveldb/db.h>
+
+namespace Firestore {
+
+std::string PrefixSuccessor(leveldb::Slice prefix) {
+ // We can increment the last character in the string and be done
+ // unless that character is 255 (0xff), in which case we have to erase the
+ // last character and increment the previous character, unless that
+ // is 255, etc. If the string is empty or consists entirely of
+ // 255's, we just return the empty string.
+ std::string limit(prefix.data(), prefix.size());
+ while (!limit.empty()) {
+ size_t index = limit.length() - 1;
+ if (limit[index] == '\xff') { // char literal avoids signed/unsigned.
+ limit.erase(index);
+ } else {
+ limit[index]++;
+ break;
+ }
+ }
+ return limit;
+}
+
+std::string ImmediateSuccessor(leveldb::Slice s) {
+ // Return the input string, with an additional NUL byte appended.
+ std::string out;
+ out.reserve(s.size() + 1);
+ out.append(s.data(), s.size());
+ out.push_back('\0');
+ return out;
+}
+
+} // namespace Firestore
diff --git a/Firestore/Port/string_util.h b/Firestore/Port/string_util.h
new file mode 100644
index 0000000..6e85ba9
--- /dev/null
+++ b/Firestore/Port/string_util.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.
+ */
+
+// Useful string functions and so forth. This is a grab-bag file.
+//
+// These functions work fine for UTF-8 strings as long as you can
+// consider them to be just byte strings. For example, due to the
+// design of UTF-8 you do not need to worry about accidental matches,
+// as long as all your inputs are valid UTF-8 (use \uHHHH, not \xHH or \oOOO).
+
+#ifndef IPHONE_FIRESTORE_PORT_STRING_UTIL_H_
+#define IPHONE_FIRESTORE_PORT_STRING_UTIL_H_
+
+#include <string>
+
+namespace leveldb {
+class Slice;
+}
+
+namespace Firestore {
+
+// Returns the smallest lexicographically larger string of equal or smaller
+// length. Returns an empty string if there is no such successor (if the input
+// is empty or consists entirely of 0xff bytes).
+// Useful for calculating the smallest lexicographically larger string
+// that will not be prefixed by the input string.
+//
+// Examples:
+// "a" -> "b", "aaa" -> "aab", "aa\xff" -> "ab", "\xff" -> "", "" -> ""
+std::string PrefixSuccessor(leveldb::Slice prefix);
+
+// Returns the immediate lexicographically-following string. This is useful to
+// turn an inclusive range into something that can be used with Bigtable's
+// SetLimitRow():
+//
+// // Inclusive range [min_element, max_element].
+// string min_element = ...;
+// string max_element = ...;
+//
+// // Equivalent range [range_start, range_end).
+// string range_start = min_element;
+// string range_end = ImmediateSuccessor(max_element);
+//
+// WARNING: Returns the input string with a '\0' appended; if you call c_str()
+// on the result, it will compare equal to s.
+//
+// WARNING: Transforms "" -> "\0"; this doesn't account for Bigtable's special
+// treatment of "" as infinity.
+std::string ImmediateSuccessor(leveldb::Slice s);
+
+} // namespace Firestore
+
+#endif // IPHONE_FIRESTORE_PORT_STRING_UTIL_H_
diff --git a/Firestore/Port/string_util_test.cc b/Firestore/Port/string_util_test.cc
new file mode 100644
index 0000000..ecdfb8f
--- /dev/null
+++ b/Firestore/Port/string_util_test.cc
@@ -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.
+ */
+
+#include "string_util.h"
+
+#include "testing/base/public/gunit.h"
+#include <leveldb/db.h>
+
+using Firestore::PrefixSuccessor;
+using Firestore::ImmediateSuccessor;
+using leveldb::Slice;
+
+TEST(Util, PrefixSuccessor) {
+ EXPECT_EQ(PrefixSuccessor("a"), "b");
+ EXPECT_EQ(PrefixSuccessor("aaAA"), "aaAB");
+ EXPECT_EQ(PrefixSuccessor("aaa\xff"), "aab");
+ EXPECT_EQ(PrefixSuccessor(string("\x00", 1)), "\x01");
+ EXPECT_EQ(PrefixSuccessor("az\xe0"), "az\xe1");
+ EXPECT_EQ(PrefixSuccessor("\xff\xff\xff"), "");
+ EXPECT_EQ(PrefixSuccessor(""), "");
+}
+
+TEST(Util, ImmediateSuccessor) {
+ EXPECT_EQ(ImmediateSuccessor("hello"), Slice("hello\0", 6));
+ EXPECT_EQ(ImmediateSuccessor(""), Slice("\0", 1));
+}
diff --git a/Firestore/Protos/FrameworkMaker.xcodeproj/project.pbxproj b/Firestore/Protos/FrameworkMaker.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..51a61b8
--- /dev/null
+++ b/Firestore/Protos/FrameworkMaker.xcodeproj/project.pbxproj
@@ -0,0 +1,428 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 621808027FC20B1A1B769E50 /* libPods-FrameworkMaker_iOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AB2E4F8834D5EA87A8F7124C /* libPods-FrameworkMaker_iOS.a */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 01F29B956E7F6E45EF34DE72 /* Pods-FrameworkMaker.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker.release.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker/Pods-FrameworkMaker.release.xcconfig"; sourceTree = "<group>"; };
+ 04058317A2F1A863FB91F84F /* Pods-FrameworkMaker_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS.release.xcconfig"; sourceTree = "<group>"; };
+ 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FrameworkMaker_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 1D25AC01A0F56F8BC5375DD2 /* libPods-FrameworkMaker.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FrameworkMaker.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 5BDF11E206B3015647181AB8 /* Pods-FrameworkMaker_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS.debug.xcconfig"; sourceTree = "<group>"; };
+ 93482F41CCA683759459AC1E /* libPods-FrameworkMaker_macOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FrameworkMaker_macOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ AB2E4F8834D5EA87A8F7124C /* libPods-FrameworkMaker_iOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FrameworkMaker_iOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ C8DA4EE8A169B227B0576C02 /* Pods-FrameworkMaker.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker/Pods-FrameworkMaker.debug.xcconfig"; sourceTree = "<group>"; };
+ D013F9FF1ED9EB9900FD68A9 /* FrameworkMaker_macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FrameworkMaker_macOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ D013FA131ED9EC0B00FD68A9 /* iOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iOS-Info.plist"; sourceTree = "<group>"; };
+ D013FA141ED9EC1500FD68A9 /* macOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "macOS-Info.plist"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 05A46BD41CC9B2BE007BDB33 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 621808027FC20B1A1B769E50 /* libPods-FrameworkMaker_iOS.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ D013F9FC1ED9EB9900FD68A9 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 05A46BCE1CC9B2BE007BDB33 = {
+ isa = PBXGroup;
+ children = (
+ D013FA131ED9EC0B00FD68A9 /* iOS-Info.plist */,
+ D013FA141ED9EC1500FD68A9 /* macOS-Info.plist */,
+ 05A46BD81CC9B2BE007BDB33 /* Products */,
+ AA03828B8B59297B5A3389B0 /* Pods */,
+ D3884AD1918E82D7FD21433D /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ 05A46BD81CC9B2BE007BDB33 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker_iOS.app */,
+ D013F9FF1ED9EB9900FD68A9 /* FrameworkMaker_macOS.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ AA03828B8B59297B5A3389B0 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ C8DA4EE8A169B227B0576C02 /* Pods-FrameworkMaker.debug.xcconfig */,
+ 01F29B956E7F6E45EF34DE72 /* Pods-FrameworkMaker.release.xcconfig */,
+ 5BDF11E206B3015647181AB8 /* Pods-FrameworkMaker_iOS.debug.xcconfig */,
+ 04058317A2F1A863FB91F84F /* Pods-FrameworkMaker_iOS.release.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "<group>";
+ };
+ D3884AD1918E82D7FD21433D /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 1D25AC01A0F56F8BC5375DD2 /* libPods-FrameworkMaker.a */,
+ AB2E4F8834D5EA87A8F7124C /* libPods-FrameworkMaker_iOS.a */,
+ 93482F41CCA683759459AC1E /* libPods-FrameworkMaker_macOS.a */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 05A46BD61CC9B2BE007BDB33 /* FrameworkMaker_iOS */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 05A46BEE1CC9B2BE007BDB33 /* Build configuration list for PBXNativeTarget "FrameworkMaker_iOS" */;
+ buildPhases = (
+ AC1C2B143A86214CE77C9932 /* [CP] Check Pods Manifest.lock */,
+ 05A46BD31CC9B2BE007BDB33 /* Sources */,
+ 05A46BD41CC9B2BE007BDB33 /* Frameworks */,
+ 05A46BD51CC9B2BE007BDB33 /* Resources */,
+ 11182BBE1E5DB1C0F58623BB /* [CP] Embed Pods Frameworks */,
+ 5040608D1004852F08A22A14 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = FrameworkMaker_iOS;
+ productName = FrameworkMaker;
+ productReference = 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker_iOS.app */;
+ productType = "com.apple.product-type.application";
+ };
+ D013F9FE1ED9EB9900FD68A9 /* FrameworkMaker_macOS */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = D013FA121ED9EB9900FD68A9 /* Build configuration list for PBXNativeTarget "FrameworkMaker_macOS" */;
+ buildPhases = (
+ D013F9FB1ED9EB9900FD68A9 /* Sources */,
+ D013F9FC1ED9EB9900FD68A9 /* Frameworks */,
+ D013F9FD1ED9EB9900FD68A9 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = FrameworkMaker_macOS;
+ productName = FrameworkMaker_macOS;
+ productReference = D013F9FF1ED9EB9900FD68A9 /* FrameworkMaker_macOS.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 05A46BCF1CC9B2BE007BDB33 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0730;
+ ORGANIZATIONNAME = "Google, Inc.";
+ TargetAttributes = {
+ 05A46BD61CC9B2BE007BDB33 = {
+ CreatedOnToolsVersion = 7.3;
+ };
+ D013F9FE1ED9EB9900FD68A9 = {
+ CreatedOnToolsVersion = 8.3.2;
+ ProvisioningStyle = Automatic;
+ };
+ };
+ };
+ buildConfigurationList = 05A46BD21CC9B2BE007BDB33 /* Build configuration list for PBXProject "FrameworkMaker" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 05A46BCE1CC9B2BE007BDB33;
+ productRefGroup = 05A46BD81CC9B2BE007BDB33 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 05A46BD61CC9B2BE007BDB33 /* FrameworkMaker_iOS */,
+ D013F9FE1ED9EB9900FD68A9 /* FrameworkMaker_macOS */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 05A46BD51CC9B2BE007BDB33 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ D013F9FD1ED9EB9900FD68A9 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 11182BBE1E5DB1C0F58623BB /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 5040608D1004852F08A22A14 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS-resources.sh",
+ "$PODS_CONFIGURATION_BUILD_DIR/gRPC/gRPCCertificates.bundle",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ AC1C2B143A86214CE77C9932 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-FrameworkMaker_iOS-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 05A46BD31CC9B2BE007BDB33 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ D013F9FB1ED9EB9900FD68A9 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 05A46BEC1CC9B2BE007BDB33 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.10;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ 05A46BED1CC9B2BE007BDB33 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.10;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 05A46BEF1CC9B2BE007BDB33 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 5BDF11E206B3015647181AB8 /* Pods-FrameworkMaker_iOS.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ INFOPLIST_FILE = "$(SRCROOT)/iOS-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = "google.FrameworkMaker-iOS";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 05A46BF01CC9B2BE007BDB33 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 04058317A2F1A863FB91F84F /* Pods-FrameworkMaker_iOS.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ INFOPLIST_FILE = "$(SRCROOT)/iOS-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = "google.FrameworkMaker-iOS";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+ D013FA101ED9EB9900FD68A9 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = "$(SRCROOT)/macOS-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.FrameworkMaker-macOS";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = macosx;
+ };
+ name = Debug;
+ };
+ D013FA111ED9EB9900FD68A9 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = "$(SRCROOT)/macOS-Info.plist";
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = "com.google.FrameworkMaker-macOS";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = macosx;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 05A46BD21CC9B2BE007BDB33 /* Build configuration list for PBXProject "FrameworkMaker" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 05A46BEC1CC9B2BE007BDB33 /* Debug */,
+ 05A46BED1CC9B2BE007BDB33 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 05A46BEE1CC9B2BE007BDB33 /* Build configuration list for PBXNativeTarget "FrameworkMaker_iOS" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 05A46BEF1CC9B2BE007BDB33 /* Debug */,
+ 05A46BF01CC9B2BE007BDB33 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ D013FA121ED9EB9900FD68A9 /* Build configuration list for PBXNativeTarget "FrameworkMaker_macOS" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ D013FA101ED9EB9900FD68A9 /* Debug */,
+ D013FA111ED9EB9900FD68A9 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 05A46BCF1CC9B2BE007BDB33 /* Project object */;
+}
diff --git a/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_iOS.xcscheme b/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_iOS.xcscheme
new file mode 100644
index 0000000..2994deb
--- /dev/null
+++ b/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_iOS.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "05A46BD61CC9B2BE007BDB33"
+ BuildableName = "FrameworkMaker_iOS.app"
+ BlueprintName = "FrameworkMaker_iOS"
+ ReferencedContainer = "container:FrameworkMaker.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "05A46BD61CC9B2BE007BDB33"
+ BuildableName = "FrameworkMaker_iOS.app"
+ BlueprintName = "FrameworkMaker_iOS"
+ ReferencedContainer = "container:FrameworkMaker.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "05A46BD61CC9B2BE007BDB33"
+ BuildableName = "FrameworkMaker_iOS.app"
+ BlueprintName = "FrameworkMaker_iOS"
+ ReferencedContainer = "container:FrameworkMaker.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "05A46BD61CC9B2BE007BDB33"
+ BuildableName = "FrameworkMaker_iOS.app"
+ BlueprintName = "FrameworkMaker_iOS"
+ ReferencedContainer = "container:FrameworkMaker.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_macOS.xcscheme b/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_macOS.xcscheme
new file mode 100644
index 0000000..dbe6579
--- /dev/null
+++ b/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_macOS.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "D013F9FE1ED9EB9900FD68A9"
+ BuildableName = "FrameworkMaker_macOS.app"
+ BlueprintName = "FrameworkMaker_macOS"
+ ReferencedContainer = "container:FrameworkMaker.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "D013F9FE1ED9EB9900FD68A9"
+ BuildableName = "FrameworkMaker_macOS.app"
+ BlueprintName = "FrameworkMaker_macOS"
+ ReferencedContainer = "container:FrameworkMaker.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "D013F9FE1ED9EB9900FD68A9"
+ BuildableName = "FrameworkMaker_macOS.app"
+ BlueprintName = "FrameworkMaker_macOS"
+ ReferencedContainer = "container:FrameworkMaker.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "D013F9FE1ED9EB9900FD68A9"
+ BuildableName = "FrameworkMaker_macOS.app"
+ BlueprintName = "FrameworkMaker_macOS"
+ ReferencedContainer = "container:FrameworkMaker.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Protos/Podfile b/Firestore/Protos/Podfile
new file mode 100644
index 0000000..988d7f8
--- /dev/null
+++ b/Firestore/Protos/Podfile
@@ -0,0 +1,11 @@
+# This Podfile and FrameworkMaker.xcodeproj is only here to access the
+# ProtoCompiler plugin.
+
+project 'FrameworkMaker.xcodeproj'
+
+target 'FrameworkMaker_iOS' do
+ platform :ios, '7.0'
+
+ # This should be versioned along with 'gRPC-ProtoRPC' in Firestore.podspec
+ pod '!ProtoCompiler-gRPCPlugin'
+end
diff --git a/Firestore/Protos/README.md b/Firestore/Protos/README.md
new file mode 100644
index 0000000..cb6d90e
--- /dev/null
+++ b/Firestore/Protos/README.md
@@ -0,0 +1,20 @@
+## Usage
+
+```
+cd firebase-ios-sdk/Firestore/Protos
+./build-protos.sh
+```
+
+Verify diffs, tests and make PR
+
+### Script Details
+
+Get the protoc and the gRPC plugin. See
+[here](https://github.com/grpc/grpc/tree/master/src/objective-c). The
+easiest way I found was to add
+`pod '!ProtoCompiler-gRPCPlugin'` to a Podfile and do `pod update`.
+
+After running the protoc, shell commands run to fix up the generated code:
+ * Flatten import paths for CocoaPods library build.
+ * Remove unneeded extensionRegistry functions.
+ * Remove non-buildable code from Annotations.pbobjc.*.
diff --git a/Firestore/Protos/build-protos.sh b/Firestore/Protos/build-protos.sh
new file mode 100755
index 0000000..4cfb12e
--- /dev/null
+++ b/Firestore/Protos/build-protos.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Copyright 2017 Google
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Run this script from firebase-ios-sdk/Firestore/Protos to regnenerate the
+# Objective C files from the protos.
+
+# pod update to install protoc and the gRPC plugin compiler.
+rm -rf Pods
+rm Podfile.lock
+pod update
+
+# Generate the objective C files from the protos.
+./Pods/!ProtoCompiler/protoc --plugin=protoc-gen-grpc=Pods/\!ProtoCompiler-gRPCPlugin/grpc_objective_c_plugin -I protos --objc_out=objc --grpc_out=objc `find protos -name *.proto -print | xargs`
+
+# CocoaPods does not like paths in library imports, flatten them.
+
+for i in `find objc -name "*.[mh]"` ; do
+ perl -i -pe 's#import ".*/#import "#' $i;
+done
+
+# Remove the unnecessary extensionRegistry functions.
+
+for i in `find objc -name "*.[m]" ` ; do
+ ./strip-registry.py $i
+done
+
+# Remove non-buildable code from Annotations.pbobjc.*.
+
+echo "static int annotations_stub __attribute__((unused,used)) = 0;" > objc/google/api/Annotations.pbobjc.m
+echo "// Empty stub file" > objc/google/api/Annotations.pbobjc.h
diff --git a/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h b/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h
new file mode 100644
index 0000000..d34090a
--- /dev/null
+++ b/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: firestore/local/maybe_document.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class FSTPBNoDocument;
+@class GCFSDocument;
+@class GPBTimestamp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTPBMaybeDocumentRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface FSTPBMaybeDocumentRoot : GPBRootObject
+@end
+
+#pragma mark - FSTPBNoDocument
+
+typedef GPB_ENUM(FSTPBNoDocument_FieldNumber) {
+ FSTPBNoDocument_FieldNumber_Name = 1,
+ FSTPBNoDocument_FieldNumber_ReadTime = 2,
+};
+
+/**
+ * A message indicating that the document is known to not exist.
+ **/
+@interface FSTPBNoDocument : GPBMessage
+
+/**
+ * The name of the document that does not exist, in the standard format:
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *name;
+
+/** The time at which we observed that it does not exist. */
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+/** Test to see if @c readTime has been set. */
+@property(nonatomic, readwrite) BOOL hasReadTime;
+
+@end
+
+#pragma mark - FSTPBMaybeDocument
+
+typedef GPB_ENUM(FSTPBMaybeDocument_FieldNumber) {
+ FSTPBMaybeDocument_FieldNumber_NoDocument = 1,
+ FSTPBMaybeDocument_FieldNumber_Document = 2,
+};
+
+typedef GPB_ENUM(FSTPBMaybeDocument_DocumentType_OneOfCase) {
+ FSTPBMaybeDocument_DocumentType_OneOfCase_GPBUnsetOneOfCase = 0,
+ FSTPBMaybeDocument_DocumentType_OneOfCase_NoDocument = 1,
+ FSTPBMaybeDocument_DocumentType_OneOfCase_Document = 2,
+};
+
+/**
+ * Represents either an existing document or the explicitly known absence of a
+ * document.
+ **/
+@interface FSTPBMaybeDocument : GPBMessage
+
+@property(nonatomic, readonly) FSTPBMaybeDocument_DocumentType_OneOfCase documentTypeOneOfCase;
+
+/** Used if the document is known to not exist. */
+@property(nonatomic, readwrite, strong, null_resettable) FSTPBNoDocument *noDocument;
+
+/** The document (if it exists). */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'documentType'.
+ **/
+void FSTPBMaybeDocument_ClearDocumentTypeOneOfCase(FSTPBMaybeDocument *message);
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.m b/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.m
new file mode 100644
index 0000000..1d4404d
--- /dev/null
+++ b/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.m
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: firestore/local/maybe_document.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Timestamp.pbobjc.h>
+#else
+ #import "Timestamp.pbobjc.h"
+#endif
+
+ #import "MaybeDocument.pbobjc.h"
+ #import "Document.pbobjc.h"
+ #import "Annotations.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#pragma clang diagnostic ignored "-Wdirect-ivar-access"
+
+#pragma mark - FSTPBMaybeDocumentRoot
+
+@implementation FSTPBMaybeDocumentRoot
+
+
+@end
+
+#pragma mark - FSTPBMaybeDocumentRoot_FileDescriptor
+
+static GPBFileDescriptor *FSTPBMaybeDocumentRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"firestore.client"
+ objcPrefix:@"FSTPB"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - FSTPBNoDocument
+
+@implementation FSTPBNoDocument
+
+@dynamic name;
+@dynamic hasReadTime, readTime;
+
+typedef struct FSTPBNoDocument__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *name;
+ GPBTimestamp *readTime;
+} FSTPBNoDocument__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "name",
+ .dataTypeSpecific.className = NULL,
+ .number = FSTPBNoDocument_FieldNumber_Name,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(FSTPBNoDocument__storage_, name),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = FSTPBNoDocument_FieldNumber_ReadTime,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(FSTPBNoDocument__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[FSTPBNoDocument class]
+ rootClass:[FSTPBMaybeDocumentRoot class]
+ file:FSTPBMaybeDocumentRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(FSTPBNoDocument__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - FSTPBMaybeDocument
+
+@implementation FSTPBMaybeDocument
+
+@dynamic documentTypeOneOfCase;
+@dynamic noDocument;
+@dynamic document;
+
+typedef struct FSTPBMaybeDocument__storage_ {
+ uint32_t _has_storage_[2];
+ FSTPBNoDocument *noDocument;
+ GCFSDocument *document;
+} FSTPBMaybeDocument__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "noDocument",
+ .dataTypeSpecific.className = GPBStringifySymbol(FSTPBNoDocument),
+ .number = FSTPBMaybeDocument_FieldNumber_NoDocument,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(FSTPBMaybeDocument__storage_, noDocument),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "document",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument),
+ .number = FSTPBMaybeDocument_FieldNumber_Document,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(FSTPBMaybeDocument__storage_, document),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[FSTPBMaybeDocument class]
+ rootClass:[FSTPBMaybeDocumentRoot class]
+ file:FSTPBMaybeDocumentRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(FSTPBMaybeDocument__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "documentType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void FSTPBMaybeDocument_ClearDocumentTypeOneOfCase(FSTPBMaybeDocument *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h b/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h
new file mode 100644
index 0000000..0089632
--- /dev/null
+++ b/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: firestore/local/mutation.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GCFSWrite;
+@class GPBTimestamp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTPBMutationRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface FSTPBMutationRoot : GPBRootObject
+@end
+
+#pragma mark - FSTPBMutationQueue
+
+typedef GPB_ENUM(FSTPBMutationQueue_FieldNumber) {
+ FSTPBMutationQueue_FieldNumber_LastAcknowledgedBatchId = 1,
+ FSTPBMutationQueue_FieldNumber_LastStreamToken = 2,
+};
+
+/**
+ * Each user gets a single queue of WriteBatches to apply to the server.
+ * MutationQueue tracks the metadata about the queue.
+ **/
+@interface FSTPBMutationQueue : GPBMessage
+
+/**
+ * An identifier for the highest numbered batch that has been acknowledged by
+ * the server. All WriteBatches in this queue with batch_ids less than or
+ * equal to this value are considered to have been acknowledged by the
+ * server.
+ **/
+@property(nonatomic, readwrite) int32_t lastAcknowledgedBatchId;
+
+/**
+ * A stream token that was previously sent by the server.
+ *
+ * See StreamingWriteRequest in datastore.proto for more details about usage.
+ *
+ * After sending this token, earlier tokens may not be used anymore so only a
+ * single stream token is retained.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *lastStreamToken;
+
+@end
+
+#pragma mark - FSTPBWriteBatch
+
+typedef GPB_ENUM(FSTPBWriteBatch_FieldNumber) {
+ FSTPBWriteBatch_FieldNumber_BatchId = 1,
+ FSTPBWriteBatch_FieldNumber_WritesArray = 2,
+ FSTPBWriteBatch_FieldNumber_LocalWriteTime = 3,
+};
+
+/**
+ * Message containing a batch of user-level writes intended to be sent to
+ * the server in a single call. Each user-level batch gets a separate
+ * WriteBatch with a new batch_id.
+ **/
+@interface FSTPBWriteBatch : GPBMessage
+
+/**
+ * An identifier for this batch, allocated by the mutation queue in a
+ * monotonically increasing manner.
+ **/
+@property(nonatomic, readwrite) int32_t batchId;
+
+/** A list of writes to apply. All writes will be applied atomically. */
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSWrite*> *writesArray;
+/** The number of items in @c writesArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger writesArray_Count;
+
+/** The local time at which the write batch was initiated. */
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *localWriteTime;
+/** Test to see if @c localWriteTime has been set. */
+@property(nonatomic, readwrite) BOOL hasLocalWriteTime;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.m b/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.m
new file mode 100644
index 0000000..8034143
--- /dev/null
+++ b/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: firestore/local/mutation.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Timestamp.pbobjc.h>
+#else
+ #import "Timestamp.pbobjc.h"
+#endif
+
+ #import "Mutation.pbobjc.h"
+ #import "Write.pbobjc.h"
+ #import "Annotations.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+#pragma mark - FSTPBMutationRoot
+
+@implementation FSTPBMutationRoot
+
+
+@end
+
+#pragma mark - FSTPBMutationRoot_FileDescriptor
+
+static GPBFileDescriptor *FSTPBMutationRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"firestore.client"
+ objcPrefix:@"FSTPB"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - FSTPBMutationQueue
+
+@implementation FSTPBMutationQueue
+
+@dynamic lastAcknowledgedBatchId;
+@dynamic lastStreamToken;
+
+typedef struct FSTPBMutationQueue__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t lastAcknowledgedBatchId;
+ NSData *lastStreamToken;
+} FSTPBMutationQueue__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "lastAcknowledgedBatchId",
+ .dataTypeSpecific.className = NULL,
+ .number = FSTPBMutationQueue_FieldNumber_LastAcknowledgedBatchId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(FSTPBMutationQueue__storage_, lastAcknowledgedBatchId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "lastStreamToken",
+ .dataTypeSpecific.className = NULL,
+ .number = FSTPBMutationQueue_FieldNumber_LastStreamToken,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(FSTPBMutationQueue__storage_, lastStreamToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[FSTPBMutationQueue class]
+ rootClass:[FSTPBMutationRoot class]
+ file:FSTPBMutationRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(FSTPBMutationQueue__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - FSTPBWriteBatch
+
+@implementation FSTPBWriteBatch
+
+@dynamic batchId;
+@dynamic writesArray, writesArray_Count;
+@dynamic hasLocalWriteTime, localWriteTime;
+
+typedef struct FSTPBWriteBatch__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t batchId;
+ NSMutableArray *writesArray;
+ GPBTimestamp *localWriteTime;
+} FSTPBWriteBatch__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "batchId",
+ .dataTypeSpecific.className = NULL,
+ .number = FSTPBWriteBatch_FieldNumber_BatchId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(FSTPBWriteBatch__storage_, batchId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "writesArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSWrite),
+ .number = FSTPBWriteBatch_FieldNumber_WritesArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(FSTPBWriteBatch__storage_, writesArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "localWriteTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = FSTPBWriteBatch_FieldNumber_LocalWriteTime,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(FSTPBWriteBatch__storage_, localWriteTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[FSTPBWriteBatch class]
+ rootClass:[FSTPBMutationRoot class]
+ file:FSTPBMutationRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(FSTPBWriteBatch__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/firestore/local/Target.pbobjc.h b/Firestore/Protos/objc/firestore/local/Target.pbobjc.h
new file mode 100644
index 0000000..d8bf49c
--- /dev/null
+++ b/Firestore/Protos/objc/firestore/local/Target.pbobjc.h
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: firestore/local/target.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GCFSTarget_DocumentsTarget;
+@class GCFSTarget_QueryTarget;
+@class GPBTimestamp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTPBTargetRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface FSTPBTargetRoot : GPBRootObject
+@end
+
+#pragma mark - FSTPBTarget
+
+typedef GPB_ENUM(FSTPBTarget_FieldNumber) {
+ FSTPBTarget_FieldNumber_TargetId = 1,
+ FSTPBTarget_FieldNumber_SnapshotVersion = 2,
+ FSTPBTarget_FieldNumber_ResumeToken = 3,
+ FSTPBTarget_FieldNumber_LastListenSequenceNumber = 4,
+ FSTPBTarget_FieldNumber_Query = 5,
+ FSTPBTarget_FieldNumber_Documents = 6,
+};
+
+typedef GPB_ENUM(FSTPBTarget_TargetType_OneOfCase) {
+ FSTPBTarget_TargetType_OneOfCase_GPBUnsetOneOfCase = 0,
+ FSTPBTarget_TargetType_OneOfCase_Query = 5,
+ FSTPBTarget_TargetType_OneOfCase_Documents = 6,
+};
+
+/**
+ * A Target is a long-lived data structure representing a resumable listen on a
+ * particular user query. While the query describes what to listen to, the
+ * Target records data about when the results were last updated and enough
+ * information to be able to resume listening later.
+ **/
+@interface FSTPBTarget : GPBMessage
+
+/**
+ * An auto-generated sequential numeric identifier for the target. This
+ * serves as the identity of the target, and once assigned never changes.
+ **/
+@property(nonatomic, readwrite) int32_t targetId;
+
+/**
+ * The last snapshot version received from the Watch Service for this target.
+ *
+ * This is the same value as TargetChange.read_time
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *snapshotVersion;
+/** Test to see if @c snapshotVersion has been set. */
+@property(nonatomic, readwrite) BOOL hasSnapshotVersion;
+
+/**
+ * An opaque, server-assigned token that allows watching a query to be
+ * resumed after disconnecting without retransmitting all the data that
+ * matches the query. The resume token essentially identifies a point in
+ * time from which the server should resume sending results.
+ *
+ * This is related to the snapshot_version in that the resume_token
+ * effectively also encodes that value, but the resume_token is opaque and
+ * sometimes encodes additional information.
+ *
+ * A consequence of this is that the resume_token should be used when asking
+ * the server to reason about where this client is in the watch stream, but
+ * the client should use the snapshot_version for its own purposes.
+ *
+ * This is the same value as TargetChange.resume_token
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *resumeToken;
+
+/**
+ * A sequence number representing the last time this query was listened to,
+ * used for garbage collection purposes.
+ *
+ * Conventionally this would be a timestamp value, but device-local clocks
+ * are unreliable and they must be able to create new listens even while
+ * disconnected. Instead this should be a monotonically increasing number
+ * that's incremented on each listen call.
+ *
+ * This is different from the target_id since the target_id is an immutable
+ * identifier assigned to the Target on first use while
+ * last_listen_sequence_number is updated every time the query is listened
+ * to.
+ **/
+@property(nonatomic, readwrite) int64_t lastListenSequenceNumber;
+
+/** The server-side type of target to listen to. */
+@property(nonatomic, readonly) FSTPBTarget_TargetType_OneOfCase targetTypeOneOfCase;
+
+/** A target specified by a query. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget_QueryTarget *query;
+
+/** A target specified by a set of document names. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget_DocumentsTarget *documents;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'targetType'.
+ **/
+void FSTPBTarget_ClearTargetTypeOneOfCase(FSTPBTarget *message);
+
+#pragma mark - FSTPBTargetGlobal
+
+typedef GPB_ENUM(FSTPBTargetGlobal_FieldNumber) {
+ FSTPBTargetGlobal_FieldNumber_HighestTargetId = 1,
+ FSTPBTargetGlobal_FieldNumber_HighestListenSequenceNumber = 2,
+ FSTPBTargetGlobal_FieldNumber_LastRemoteSnapshotVersion = 3,
+};
+
+/**
+ * Global state tracked across all Targets, tracked separately to avoid the
+ * need for extra indexes.
+ **/
+@interface FSTPBTargetGlobal : GPBMessage
+
+/**
+ * The highest numbered target id across all Targets.
+ *
+ * See Target.target_id.
+ **/
+@property(nonatomic, readwrite) int32_t highestTargetId;
+
+/**
+ * The highest numbered last_listen_sequence_number across all Targets.
+ *
+ * See Target.last_listen_sequence_number.
+ **/
+@property(nonatomic, readwrite) int64_t highestListenSequenceNumber;
+
+/**
+ * A global snapshot version representing the last consistent snapshot we
+ * received from the backend. This is monotonically increasing and any
+ * snapshots received from the backend prior to this version (e.g. for
+ * targets resumed with a resume_token) should be suppressed (buffered) until
+ * the backend has caught up to this snapshot_version again. This prevents
+ * our cache from ever going backwards in time.
+ *
+ * This is updated whenever our we get a TargetChange with a read_time and
+ * empty target_ids.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *lastRemoteSnapshotVersion;
+/** Test to see if @c lastRemoteSnapshotVersion has been set. */
+@property(nonatomic, readwrite) BOOL hasLastRemoteSnapshotVersion;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/firestore/local/Target.pbobjc.m b/Firestore/Protos/objc/firestore/local/Target.pbobjc.m
new file mode 100644
index 0000000..6f6ccf2
--- /dev/null
+++ b/Firestore/Protos/objc/firestore/local/Target.pbobjc.m
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: firestore/local/target.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Timestamp.pbobjc.h>
+#else
+ #import "Timestamp.pbobjc.h"
+#endif
+
+ #import "Target.pbobjc.h"
+ #import "Firestore.pbobjc.h"
+ #import "Annotations.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#pragma clang diagnostic ignored "-Wdirect-ivar-access"
+
+#pragma mark - FSTPBTargetRoot
+
+@implementation FSTPBTargetRoot
+
+
+@end
+
+#pragma mark - FSTPBTargetRoot_FileDescriptor
+
+static GPBFileDescriptor *FSTPBTargetRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"firestore.client"
+ objcPrefix:@"FSTPB"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - FSTPBTarget
+
+@implementation FSTPBTarget
+
+@dynamic targetTypeOneOfCase;
+@dynamic targetId;
+@dynamic hasSnapshotVersion, snapshotVersion;
+@dynamic resumeToken;
+@dynamic lastListenSequenceNumber;
+@dynamic query;
+@dynamic documents;
+
+typedef struct FSTPBTarget__storage_ {
+ uint32_t _has_storage_[2];
+ int32_t targetId;
+ GPBTimestamp *snapshotVersion;
+ NSData *resumeToken;
+ GCFSTarget_QueryTarget *query;
+ GCFSTarget_DocumentsTarget *documents;
+ int64_t lastListenSequenceNumber;
+} FSTPBTarget__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "targetId",
+ .dataTypeSpecific.className = NULL,
+ .number = FSTPBTarget_FieldNumber_TargetId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(FSTPBTarget__storage_, targetId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "snapshotVersion",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = FSTPBTarget_FieldNumber_SnapshotVersion,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(FSTPBTarget__storage_, snapshotVersion),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "resumeToken",
+ .dataTypeSpecific.className = NULL,
+ .number = FSTPBTarget_FieldNumber_ResumeToken,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(FSTPBTarget__storage_, resumeToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "lastListenSequenceNumber",
+ .dataTypeSpecific.className = NULL,
+ .number = FSTPBTarget_FieldNumber_LastListenSequenceNumber,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(FSTPBTarget__storage_, lastListenSequenceNumber),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "query",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget_QueryTarget),
+ .number = FSTPBTarget_FieldNumber_Query,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(FSTPBTarget__storage_, query),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "documents",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget_DocumentsTarget),
+ .number = FSTPBTarget_FieldNumber_Documents,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(FSTPBTarget__storage_, documents),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[FSTPBTarget class]
+ rootClass:[FSTPBTargetRoot class]
+ file:FSTPBTargetRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(FSTPBTarget__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "targetType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void FSTPBTarget_ClearTargetTypeOneOfCase(FSTPBTarget *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - FSTPBTargetGlobal
+
+@implementation FSTPBTargetGlobal
+
+@dynamic highestTargetId;
+@dynamic highestListenSequenceNumber;
+@dynamic hasLastRemoteSnapshotVersion, lastRemoteSnapshotVersion;
+
+typedef struct FSTPBTargetGlobal__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t highestTargetId;
+ GPBTimestamp *lastRemoteSnapshotVersion;
+ int64_t highestListenSequenceNumber;
+} FSTPBTargetGlobal__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "highestTargetId",
+ .dataTypeSpecific.className = NULL,
+ .number = FSTPBTargetGlobal_FieldNumber_HighestTargetId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(FSTPBTargetGlobal__storage_, highestTargetId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "highestListenSequenceNumber",
+ .dataTypeSpecific.className = NULL,
+ .number = FSTPBTargetGlobal_FieldNumber_HighestListenSequenceNumber,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(FSTPBTargetGlobal__storage_, highestListenSequenceNumber),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "lastRemoteSnapshotVersion",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = FSTPBTargetGlobal_FieldNumber_LastRemoteSnapshotVersion,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(FSTPBTargetGlobal__storage_, lastRemoteSnapshotVersion),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[FSTPBTargetGlobal class]
+ rootClass:[FSTPBTargetRoot class]
+ file:FSTPBTargetRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(FSTPBTargetGlobal__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/api/Annotations.pbobjc.h b/Firestore/Protos/objc/google/api/Annotations.pbobjc.h
new file mode 100644
index 0000000..b7bee2d
--- /dev/null
+++ b/Firestore/Protos/objc/google/api/Annotations.pbobjc.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Empty stub file
diff --git a/Firestore/Protos/objc/google/api/Annotations.pbobjc.m b/Firestore/Protos/objc/google/api/Annotations.pbobjc.m
new file mode 100644
index 0000000..ef0558d
--- /dev/null
+++ b/Firestore/Protos/objc/google/api/Annotations.pbobjc.m
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+static int annotations_stub __attribute__((unused,used)) = 0;
diff --git a/Firestore/Protos/objc/google/api/HTTP.pbobjc.h b/Firestore/Protos/objc/google/api/HTTP.pbobjc.h
new file mode 100644
index 0000000..9cc00dc
--- /dev/null
+++ b/Firestore/Protos/objc/google/api/HTTP.pbobjc.h
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/api/http.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GAPICustomHttpPattern;
+@class GAPIHttpRule;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - GAPIHTTPRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GAPIHTTPRoot : GPBRootObject
+@end
+
+#pragma mark - GAPIHttp
+
+typedef GPB_ENUM(GAPIHttp_FieldNumber) {
+ GAPIHttp_FieldNumber_RulesArray = 1,
+};
+
+/**
+ * Defines the HTTP configuration for a service. It contains a list of
+ * [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
+ * to one or more HTTP REST API methods.
+ **/
+@interface GAPIHttp : GPBMessage
+
+/**
+ * A list of HTTP configuration rules that apply to individual API methods.
+ *
+ * **NOTE:** All service configuration rules follow "last one wins" order.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GAPIHttpRule*> *rulesArray;
+/** The number of items in @c rulesArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger rulesArray_Count;
+
+@end
+
+#pragma mark - GAPIHttpRule
+
+typedef GPB_ENUM(GAPIHttpRule_FieldNumber) {
+ GAPIHttpRule_FieldNumber_Selector = 1,
+ GAPIHttpRule_FieldNumber_Get = 2,
+ GAPIHttpRule_FieldNumber_Put = 3,
+ GAPIHttpRule_FieldNumber_Post = 4,
+ GAPIHttpRule_FieldNumber_Delete_p = 5,
+ GAPIHttpRule_FieldNumber_Patch = 6,
+ GAPIHttpRule_FieldNumber_Body = 7,
+ GAPIHttpRule_FieldNumber_Custom = 8,
+ GAPIHttpRule_FieldNumber_AdditionalBindingsArray = 11,
+};
+
+typedef GPB_ENUM(GAPIHttpRule_Pattern_OneOfCase) {
+ GAPIHttpRule_Pattern_OneOfCase_GPBUnsetOneOfCase = 0,
+ GAPIHttpRule_Pattern_OneOfCase_Get = 2,
+ GAPIHttpRule_Pattern_OneOfCase_Put = 3,
+ GAPIHttpRule_Pattern_OneOfCase_Post = 4,
+ GAPIHttpRule_Pattern_OneOfCase_Delete_p = 5,
+ GAPIHttpRule_Pattern_OneOfCase_Patch = 6,
+ GAPIHttpRule_Pattern_OneOfCase_Custom = 8,
+};
+
+/**
+ * `HttpRule` defines the mapping of an RPC method to one or more HTTP
+ * REST APIs. The mapping determines what portions of the request
+ * message are populated from the path, query parameters, or body of
+ * the HTTP request. The mapping is typically specified as an
+ * `google.api.http` annotation, see "google/api/annotations.proto"
+ * for details.
+ *
+ * The mapping consists of a field specifying the path template and
+ * method kind. The path template can refer to fields in the request
+ * message, as in the example below which describes a REST GET
+ * operation on a resource collection of messages:
+ *
+ *
+ * service Messaging {
+ * rpc GetMessage(GetMessageRequest) returns (Message) {
+ * option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}";
+ * }
+ * }
+ * message GetMessageRequest {
+ * message SubMessage {
+ * string subfield = 1;
+ * }
+ * string message_id = 1; // mapped to the URL
+ * SubMessage sub = 2; // `sub.subfield` is url-mapped
+ * }
+ * message Message {
+ * string text = 1; // content of the resource
+ * }
+ *
+ * The same http annotation can alternatively be expressed inside the
+ * `GRPC API Configuration` YAML file.
+ *
+ * http:
+ * rules:
+ * - selector: <proto_package_name>.Messaging.GetMessage
+ * get: /v1/messages/{message_id}/{sub.subfield}
+ *
+ * This definition enables an automatic, bidrectional mapping of HTTP
+ * JSON to RPC. Example:
+ *
+ * HTTP | RPC
+ * -----|-----
+ * `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))`
+ *
+ * In general, not only fields but also field paths can be referenced
+ * from a path pattern. Fields mapped to the path pattern cannot be
+ * repeated and must have a primitive (non-message) type.
+ *
+ * Any fields in the request message which are not bound by the path
+ * pattern automatically become (optional) HTTP query
+ * parameters. Assume the following definition of the request message:
+ *
+ *
+ * message GetMessageRequest {
+ * message SubMessage {
+ * string subfield = 1;
+ * }
+ * string message_id = 1; // mapped to the URL
+ * int64 revision = 2; // becomes a parameter
+ * SubMessage sub = 3; // `sub.subfield` becomes a parameter
+ * }
+ *
+ *
+ * This enables a HTTP JSON to RPC mapping as below:
+ *
+ * HTTP | RPC
+ * -----|-----
+ * `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))`
+ *
+ * Note that fields which are mapped to HTTP parameters must have a
+ * primitive type or a repeated primitive type. Message types are not
+ * allowed. In the case of a repeated type, the parameter can be
+ * repeated in the URL, as in `...?param=A&param=B`.
+ *
+ * For HTTP method kinds which allow a request body, the `body` field
+ * specifies the mapping. Consider a REST update method on the
+ * message resource collection:
+ *
+ *
+ * service Messaging {
+ * rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
+ * option (google.api.http) = {
+ * put: "/v1/messages/{message_id}"
+ * body: "message"
+ * };
+ * }
+ * }
+ * message UpdateMessageRequest {
+ * string message_id = 1; // mapped to the URL
+ * Message message = 2; // mapped to the body
+ * }
+ *
+ *
+ * The following HTTP JSON to RPC mapping is enabled, where the
+ * representation of the JSON in the request body is determined by
+ * protos JSON encoding:
+ *
+ * HTTP | RPC
+ * -----|-----
+ * `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })`
+ *
+ * The special name `*` can be used in the body mapping to define that
+ * every field not bound by the path template should be mapped to the
+ * request body. This enables the following alternative definition of
+ * the update method:
+ *
+ * service Messaging {
+ * rpc UpdateMessage(Message) returns (Message) {
+ * option (google.api.http) = {
+ * put: "/v1/messages/{message_id}"
+ * body: "*"
+ * };
+ * }
+ * }
+ * message Message {
+ * string message_id = 1;
+ * string text = 2;
+ * }
+ *
+ *
+ * The following HTTP JSON to RPC mapping is enabled:
+ *
+ * HTTP | RPC
+ * -----|-----
+ * `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")`
+ *
+ * Note that when using `*` in the body mapping, it is not possible to
+ * have HTTP parameters, as all fields not bound by the path end in
+ * the body. This makes this option more rarely used in practice of
+ * defining REST APIs. The common usage of `*` is in custom methods
+ * which don't use the URL at all for transferring data.
+ *
+ * It is possible to define multiple HTTP methods for one RPC by using
+ * the `additional_bindings` option. Example:
+ *
+ * service Messaging {
+ * rpc GetMessage(GetMessageRequest) returns (Message) {
+ * option (google.api.http) = {
+ * get: "/v1/messages/{message_id}"
+ * additional_bindings {
+ * get: "/v1/users/{user_id}/messages/{message_id}"
+ * }
+ * };
+ * }
+ * }
+ * message GetMessageRequest {
+ * string message_id = 1;
+ * string user_id = 2;
+ * }
+ *
+ *
+ * This enables the following two alternative HTTP JSON to RPC
+ * mappings:
+ *
+ * HTTP | RPC
+ * -----|-----
+ * `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
+ * `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")`
+ *
+ * # Rules for HTTP mapping
+ *
+ * The rules for mapping HTTP path, query parameters, and body fields
+ * to the request message are as follows:
+ *
+ * 1. The `body` field specifies either `*` or a field path, or is
+ * omitted. If omitted, it assumes there is no HTTP body.
+ * 2. Leaf fields (recursive expansion of nested messages in the
+ * request) can be classified into three types:
+ * (a) Matched in the URL template.
+ * (b) Covered by body (if body is `*`, everything except (a) fields;
+ * else everything under the body field)
+ * (c) All other fields.
+ * 3. URL query parameters found in the HTTP request are mapped to (c) fields.
+ * 4. Any body sent with an HTTP request can contain only (b) fields.
+ *
+ * The syntax of the path template is as follows:
+ *
+ * Template = "/" Segments [ Verb ] ;
+ * Segments = Segment { "/" Segment } ;
+ * Segment = "*" | "**" | LITERAL | Variable ;
+ * Variable = "{" FieldPath [ "=" Segments ] "}" ;
+ * FieldPath = IDENT { "." IDENT } ;
+ * Verb = ":" LITERAL ;
+ *
+ * The syntax `*` matches a single path segment. It follows the semantics of
+ * [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String
+ * Expansion.
+ *
+ * The syntax `**` matches zero or more path segments. It follows the semantics
+ * of [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.3 Reserved
+ * Expansion. NOTE: it must be the last segment in the path except the Verb.
+ *
+ * The syntax `LITERAL` matches literal text in the URL path.
+ *
+ * The syntax `Variable` matches the entire path as specified by its template;
+ * this nested template must not contain further variables. If a variable
+ * matches a single path segment, its template may be omitted, e.g. `{var}`
+ * is equivalent to `{var=*}`.
+ *
+ * NOTE: the field paths in variables and in the `body` must not refer to
+ * repeated fields or map fields.
+ *
+ * Use CustomHttpPattern to specify any HTTP method that is not included in the
+ * `pattern` field, such as HEAD, or "*" to leave the HTTP method unspecified for
+ * a given URL path rule. The wild-card rule is useful for services that provide
+ * content to Web (HTML) clients.
+ **/
+@interface GAPIHttpRule : GPBMessage
+
+/**
+ * Selects methods to which this rule applies.
+ *
+ * Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *selector;
+
+/**
+ * Determines the URL pattern is matched by this rules. This pattern can be
+ * used with any of the {get|put|post|delete|patch} methods. A custom method
+ * can be defined using the 'custom' field.
+ **/
+@property(nonatomic, readonly) GAPIHttpRule_Pattern_OneOfCase patternOneOfCase;
+
+/** Used for listing and getting information about resources. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *get;
+
+/** Used for updating a resource. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *put;
+
+/** Used for creating a resource. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *post;
+
+/** Used for deleting a resource. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *delete_p;
+
+/** Used for updating a resource. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *patch;
+
+/** Custom pattern is used for defining custom verbs. */
+@property(nonatomic, readwrite, strong, null_resettable) GAPICustomHttpPattern *custom;
+
+/**
+ * The name of the request field whose value is mapped to the HTTP body, or
+ * `*` for mapping all fields not captured by the path pattern to the HTTP
+ * body. NOTE: the referred field must not be a repeated field and must be
+ * present at the top-level of request message type.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *body;
+
+/**
+ * Additional HTTP bindings for the selector. Nested bindings must
+ * not contain an `additional_bindings` field themselves (that is,
+ * the nesting may only be one level deep).
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GAPIHttpRule*> *additionalBindingsArray;
+/** The number of items in @c additionalBindingsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger additionalBindingsArray_Count;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'pattern'.
+ **/
+void GAPIHttpRule_ClearPatternOneOfCase(GAPIHttpRule *message);
+
+#pragma mark - GAPICustomHttpPattern
+
+typedef GPB_ENUM(GAPICustomHttpPattern_FieldNumber) {
+ GAPICustomHttpPattern_FieldNumber_Kind = 1,
+ GAPICustomHttpPattern_FieldNumber_Path = 2,
+};
+
+/**
+ * A custom pattern is used for defining custom HTTP verb.
+ **/
+@interface GAPICustomHttpPattern : GPBMessage
+
+/** The name of this custom HTTP verb. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *kind;
+
+/** The path matched by this custom verb. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *path;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/api/HTTP.pbobjc.m b/Firestore/Protos/objc/google/api/HTTP.pbobjc.m
new file mode 100644
index 0000000..5adf41c
--- /dev/null
+++ b/Firestore/Protos/objc/google/api/HTTP.pbobjc.m
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/api/http.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+ #import "HTTP.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#pragma clang diagnostic ignored "-Wdirect-ivar-access"
+
+#pragma mark - GAPIHTTPRoot
+
+@implementation GAPIHTTPRoot
+
+// No extensions in the file and no imports, so no need to generate
+// +extensionRegistry.
+
+@end
+
+#pragma mark - GAPIHTTPRoot_FileDescriptor
+
+static GPBFileDescriptor *GAPIHTTPRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.api"
+ objcPrefix:@"GAPI"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - GAPIHttp
+
+@implementation GAPIHttp
+
+@dynamic rulesArray, rulesArray_Count;
+
+typedef struct GAPIHttp__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *rulesArray;
+} GAPIHttp__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "rulesArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GAPIHttpRule),
+ .number = GAPIHttp_FieldNumber_RulesArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GAPIHttp__storage_, rulesArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GAPIHttp class]
+ rootClass:[GAPIHTTPRoot class]
+ file:GAPIHTTPRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GAPIHttp__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GAPIHttpRule
+
+@implementation GAPIHttpRule
+
+@dynamic patternOneOfCase;
+@dynamic selector;
+@dynamic get;
+@dynamic put;
+@dynamic post;
+@dynamic delete_p;
+@dynamic patch;
+@dynamic custom;
+@dynamic body;
+@dynamic additionalBindingsArray, additionalBindingsArray_Count;
+
+typedef struct GAPIHttpRule__storage_ {
+ uint32_t _has_storage_[2];
+ NSString *selector;
+ NSString *get;
+ NSString *put;
+ NSString *post;
+ NSString *delete_p;
+ NSString *patch;
+ NSString *body;
+ GAPICustomHttpPattern *custom;
+ NSMutableArray *additionalBindingsArray;
+} GAPIHttpRule__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "selector",
+ .dataTypeSpecific.className = NULL,
+ .number = GAPIHttpRule_FieldNumber_Selector,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, selector),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "get",
+ .dataTypeSpecific.className = NULL,
+ .number = GAPIHttpRule_FieldNumber_Get,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, get),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "put",
+ .dataTypeSpecific.className = NULL,
+ .number = GAPIHttpRule_FieldNumber_Put,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, put),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "post",
+ .dataTypeSpecific.className = NULL,
+ .number = GAPIHttpRule_FieldNumber_Post,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, post),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "delete_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GAPIHttpRule_FieldNumber_Delete_p,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, delete_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "patch",
+ .dataTypeSpecific.className = NULL,
+ .number = GAPIHttpRule_FieldNumber_Patch,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, patch),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "body",
+ .dataTypeSpecific.className = NULL,
+ .number = GAPIHttpRule_FieldNumber_Body,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, body),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "custom",
+ .dataTypeSpecific.className = GPBStringifySymbol(GAPICustomHttpPattern),
+ .number = GAPIHttpRule_FieldNumber_Custom,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, custom),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "additionalBindingsArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GAPIHttpRule),
+ .number = GAPIHttpRule_FieldNumber_AdditionalBindingsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, additionalBindingsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GAPIHttpRule class]
+ rootClass:[GAPIHTTPRoot class]
+ file:GAPIHTTPRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GAPIHttpRule__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "pattern",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GAPIHttpRule_ClearPatternOneOfCase(GAPIHttpRule *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GAPICustomHttpPattern
+
+@implementation GAPICustomHttpPattern
+
+@dynamic kind;
+@dynamic path;
+
+typedef struct GAPICustomHttpPattern__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *kind;
+ NSString *path;
+} GAPICustomHttpPattern__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "kind",
+ .dataTypeSpecific.className = NULL,
+ .number = GAPICustomHttpPattern_FieldNumber_Kind,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GAPICustomHttpPattern__storage_, kind),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "path",
+ .dataTypeSpecific.className = NULL,
+ .number = GAPICustomHttpPattern_FieldNumber_Path,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GAPICustomHttpPattern__storage_, path),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GAPICustomHttpPattern class]
+ rootClass:[GAPIHTTPRoot class]
+ file:GAPIHTTPRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GAPICustomHttpPattern__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h
new file mode 100644
index 0000000..6215e82
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h
@@ -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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/common.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GCFSTransactionOptions_ReadOnly;
+@class GCFSTransactionOptions_ReadWrite;
+@class GPBTimestamp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - GCFSCommonRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GCFSCommonRoot : GPBRootObject
+@end
+
+#pragma mark - GCFSDocumentMask
+
+typedef GPB_ENUM(GCFSDocumentMask_FieldNumber) {
+ GCFSDocumentMask_FieldNumber_FieldPathsArray = 1,
+};
+
+/**
+ * A set of field paths on a document.
+ * Used to restrict a get or update operation on a document to a subset of its
+ * fields.
+ * This is different from standard field masks, as this is always scoped to a
+ * [Document][google.firestore.v1beta1.Document], and takes in account the dynamic nature of [Value][google.firestore.v1beta1.Value].
+ **/
+@interface GCFSDocumentMask : GPBMessage
+
+/**
+ * The list of field paths in the mask. See [Document.fields][google.firestore.v1beta1.Document.fields] for a field
+ * path syntax reference.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *fieldPathsArray;
+/** The number of items in @c fieldPathsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger fieldPathsArray_Count;
+
+@end
+
+#pragma mark - GCFSPrecondition
+
+typedef GPB_ENUM(GCFSPrecondition_FieldNumber) {
+ GCFSPrecondition_FieldNumber_Exists = 1,
+ GCFSPrecondition_FieldNumber_UpdateTime = 2,
+};
+
+typedef GPB_ENUM(GCFSPrecondition_ConditionType_OneOfCase) {
+ GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSPrecondition_ConditionType_OneOfCase_Exists = 1,
+ GCFSPrecondition_ConditionType_OneOfCase_UpdateTime = 2,
+};
+
+/**
+ * A precondition on a document, used for conditional operations.
+ **/
+@interface GCFSPrecondition : GPBMessage
+
+/** The type of precondition. */
+@property(nonatomic, readonly) GCFSPrecondition_ConditionType_OneOfCase conditionTypeOneOfCase;
+
+/**
+ * When set to `true`, the target document must exist.
+ * When set to `false`, the target document must not exist.
+ **/
+@property(nonatomic, readwrite) BOOL exists;
+
+/**
+ * When set, the target document must exist and have been last updated at
+ * that time.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *updateTime;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'conditionType'.
+ **/
+void GCFSPrecondition_ClearConditionTypeOneOfCase(GCFSPrecondition *message);
+
+#pragma mark - GCFSTransactionOptions
+
+typedef GPB_ENUM(GCFSTransactionOptions_FieldNumber) {
+ GCFSTransactionOptions_FieldNumber_ReadOnly = 2,
+ GCFSTransactionOptions_FieldNumber_ReadWrite = 3,
+};
+
+typedef GPB_ENUM(GCFSTransactionOptions_Mode_OneOfCase) {
+ GCFSTransactionOptions_Mode_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSTransactionOptions_Mode_OneOfCase_ReadOnly = 2,
+ GCFSTransactionOptions_Mode_OneOfCase_ReadWrite = 3,
+};
+
+/**
+ * Options for creating a new transaction.
+ **/
+@interface GCFSTransactionOptions : GPBMessage
+
+/** The mode of the transaction. */
+@property(nonatomic, readonly) GCFSTransactionOptions_Mode_OneOfCase modeOneOfCase;
+
+/** The transaction can only be used for read operations. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions_ReadOnly *readOnly;
+
+/** The transaction can be used for both read and write operations. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions_ReadWrite *readWrite;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'mode'.
+ **/
+void GCFSTransactionOptions_ClearModeOneOfCase(GCFSTransactionOptions *message);
+
+#pragma mark - GCFSTransactionOptions_ReadWrite
+
+typedef GPB_ENUM(GCFSTransactionOptions_ReadWrite_FieldNumber) {
+ GCFSTransactionOptions_ReadWrite_FieldNumber_RetryTransaction = 1,
+};
+
+/**
+ * Options for a transaction that can be used to read and write documents.
+ **/
+@interface GCFSTransactionOptions_ReadWrite : GPBMessage
+
+/** An optional transaction to retry. */
+@property(nonatomic, readwrite, copy, null_resettable) NSData *retryTransaction;
+
+@end
+
+#pragma mark - GCFSTransactionOptions_ReadOnly
+
+typedef GPB_ENUM(GCFSTransactionOptions_ReadOnly_FieldNumber) {
+ GCFSTransactionOptions_ReadOnly_FieldNumber_ReadTime = 2,
+};
+
+typedef GPB_ENUM(GCFSTransactionOptions_ReadOnly_ConsistencySelector_OneOfCase) {
+ GCFSTransactionOptions_ReadOnly_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSTransactionOptions_ReadOnly_ConsistencySelector_OneOfCase_ReadTime = 2,
+};
+
+/**
+ * Options for a transaction that can only be used to read documents.
+ **/
+@interface GCFSTransactionOptions_ReadOnly : GPBMessage
+
+/**
+ * The consistency mode for this transaction. If not set, defaults to strong
+ * consistency.
+ **/
+@property(nonatomic, readonly) GCFSTransactionOptions_ReadOnly_ConsistencySelector_OneOfCase consistencySelectorOneOfCase;
+
+/**
+ * Reads documents at the given time.
+ * This may not be older than 60 seconds.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'consistencySelector'.
+ **/
+void GCFSTransactionOptions_ReadOnly_ClearConsistencySelectorOneOfCase(GCFSTransactionOptions_ReadOnly *message);
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.m
new file mode 100644
index 0000000..118f56e
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.m
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/common.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Timestamp.pbobjc.h>
+#else
+ #import "Timestamp.pbobjc.h"
+#endif
+
+ #import "Common.pbobjc.h"
+ #import "Annotations.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#pragma clang diagnostic ignored "-Wdirect-ivar-access"
+
+#pragma mark - GCFSCommonRoot
+
+@implementation GCFSCommonRoot
+
+
+@end
+
+#pragma mark - GCFSCommonRoot_FileDescriptor
+
+static GPBFileDescriptor *GCFSCommonRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1"
+ objcPrefix:@"GCFS"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - GCFSDocumentMask
+
+@implementation GCFSDocumentMask
+
+@dynamic fieldPathsArray, fieldPathsArray_Count;
+
+typedef struct GCFSDocumentMask__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *fieldPathsArray;
+} GCFSDocumentMask__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "fieldPathsArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocumentMask_FieldNumber_FieldPathsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSDocumentMask__storage_, fieldPathsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSDocumentMask class]
+ rootClass:[GCFSCommonRoot class]
+ file:GCFSCommonRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSDocumentMask__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSPrecondition
+
+@implementation GCFSPrecondition
+
+@dynamic conditionTypeOneOfCase;
+@dynamic exists;
+@dynamic updateTime;
+
+typedef struct GCFSPrecondition__storage_ {
+ uint32_t _has_storage_[2];
+ GPBTimestamp *updateTime;
+} GCFSPrecondition__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "exists",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSPrecondition_FieldNumber_Exists,
+ .hasIndex = -1,
+ .offset = 0, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "updateTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSPrecondition_FieldNumber_UpdateTime,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSPrecondition__storage_, updateTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSPrecondition class]
+ rootClass:[GCFSCommonRoot class]
+ file:GCFSCommonRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSPrecondition__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "conditionType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSPrecondition_ClearConditionTypeOneOfCase(GCFSPrecondition *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSTransactionOptions
+
+@implementation GCFSTransactionOptions
+
+@dynamic modeOneOfCase;
+@dynamic readOnly;
+@dynamic readWrite;
+
+typedef struct GCFSTransactionOptions__storage_ {
+ uint32_t _has_storage_[2];
+ GCFSTransactionOptions_ReadOnly *readOnly;
+ GCFSTransactionOptions_ReadWrite *readWrite;
+} GCFSTransactionOptions__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "readOnly",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions_ReadOnly),
+ .number = GCFSTransactionOptions_FieldNumber_ReadOnly,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSTransactionOptions__storage_, readOnly),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "readWrite",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions_ReadWrite),
+ .number = GCFSTransactionOptions_FieldNumber_ReadWrite,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSTransactionOptions__storage_, readWrite),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSTransactionOptions class]
+ rootClass:[GCFSCommonRoot class]
+ file:GCFSCommonRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSTransactionOptions__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "mode",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSTransactionOptions_ClearModeOneOfCase(GCFSTransactionOptions *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSTransactionOptions_ReadWrite
+
+@implementation GCFSTransactionOptions_ReadWrite
+
+@dynamic retryTransaction;
+
+typedef struct GCFSTransactionOptions_ReadWrite__storage_ {
+ uint32_t _has_storage_[1];
+ NSData *retryTransaction;
+} GCFSTransactionOptions_ReadWrite__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "retryTransaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSTransactionOptions_ReadWrite_FieldNumber_RetryTransaction,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSTransactionOptions_ReadWrite__storage_, retryTransaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSTransactionOptions_ReadWrite class]
+ rootClass:[GCFSCommonRoot class]
+ file:GCFSCommonRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSTransactionOptions_ReadWrite__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSTransactionOptions)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSTransactionOptions_ReadOnly
+
+@implementation GCFSTransactionOptions_ReadOnly
+
+@dynamic consistencySelectorOneOfCase;
+@dynamic readTime;
+
+typedef struct GCFSTransactionOptions_ReadOnly__storage_ {
+ uint32_t _has_storage_[2];
+ GPBTimestamp *readTime;
+} GCFSTransactionOptions_ReadOnly__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSTransactionOptions_ReadOnly_FieldNumber_ReadTime,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSTransactionOptions_ReadOnly__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSTransactionOptions_ReadOnly class]
+ rootClass:[GCFSCommonRoot class]
+ file:GCFSCommonRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSTransactionOptions_ReadOnly__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "consistencySelector",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSTransactionOptions)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSTransactionOptions_ReadOnly_ClearConsistencySelectorOneOfCase(GCFSTransactionOptions_ReadOnly *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h
new file mode 100644
index 0000000..3c5bfb1
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/document.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GCFSArrayValue;
+@class GCFSMapValue;
+@class GCFSValue;
+@class GPBTimestamp;
+@class GTPLatLng;
+GPB_ENUM_FWD_DECLARE(GPBNullValue);
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - GCFSDocumentRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GCFSDocumentRoot : GPBRootObject
+@end
+
+#pragma mark - GCFSDocument
+
+typedef GPB_ENUM(GCFSDocument_FieldNumber) {
+ GCFSDocument_FieldNumber_Name = 1,
+ GCFSDocument_FieldNumber_Fields = 2,
+ GCFSDocument_FieldNumber_CreateTime = 3,
+ GCFSDocument_FieldNumber_UpdateTime = 4,
+};
+
+/**
+ * A Firestore document.
+ *
+ * Must not exceed 1 MiB - 4 bytes.
+ **/
+@interface GCFSDocument : GPBMessage
+
+/**
+ * The resource name of the document, for example
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *name;
+
+/**
+ * The document's fields.
+ *
+ * The map keys represent field names.
+ *
+ * A simple field name contains only characters `a` to `z`, `A` to `Z`,
+ * `0` to `9`, or `_`, and must not start with `0` to `9` or `_`. For example,
+ * `foo_bar_17`.
+ *
+ * Field names matching the regular expression `__.*__` are reserved. Reserved
+ * field names are forbidden except in certain documented contexts. The map
+ * keys, represented as UTF-8, must not exceed 1,500 bytes and cannot be
+ * empty.
+ *
+ * Field paths may be used in other contexts to refer to structured fields
+ * defined here. For `map_value`, the field path is represented by the simple
+ * or quoted field names of the containing fields, delimited by `.`. For
+ * example, the structured field
+ * `"foo" : { map_value: { "x&y" : { string_value: "hello" }}}` would be
+ * represented by the field path `foo.x&y`.
+ *
+ * Within a field path, a quoted field name starts and ends with `` ` `` and
+ * may contain any character. Some characters, including `` ` ``, must be
+ * escaped using a `\\`. For example, `` `x&y` `` represents `x&y` and
+ * `` `bak\\`tik` `` represents `` bak`tik ``.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableDictionary<NSString*, GCFSValue*> *fields;
+/** The number of items in @c fields without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger fields_Count;
+
+/**
+ * Output only. The time at which the document was created.
+ *
+ * This value increases monotonically when a document is deleted then
+ * recreated. It can also be compared to values from other documents and
+ * the `read_time` of a query.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *createTime;
+/** Test to see if @c createTime has been set. */
+@property(nonatomic, readwrite) BOOL hasCreateTime;
+
+/**
+ * Output only. The time at which the document was last changed.
+ *
+ * This value is initally set to the `create_time` then increases
+ * monotonically with each change to the document. It can also be
+ * compared to values from other documents and the `read_time` of a query.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *updateTime;
+/** Test to see if @c updateTime has been set. */
+@property(nonatomic, readwrite) BOOL hasUpdateTime;
+
+@end
+
+#pragma mark - GCFSValue
+
+typedef GPB_ENUM(GCFSValue_FieldNumber) {
+ GCFSValue_FieldNumber_BooleanValue = 1,
+ GCFSValue_FieldNumber_IntegerValue = 2,
+ GCFSValue_FieldNumber_DoubleValue = 3,
+ GCFSValue_FieldNumber_ReferenceValue = 5,
+ GCFSValue_FieldNumber_MapValue = 6,
+ GCFSValue_FieldNumber_GeoPointValue = 8,
+ GCFSValue_FieldNumber_ArrayValue = 9,
+ GCFSValue_FieldNumber_TimestampValue = 10,
+ GCFSValue_FieldNumber_NullValue = 11,
+ GCFSValue_FieldNumber_StringValue = 17,
+ GCFSValue_FieldNumber_BytesValue = 18,
+};
+
+typedef GPB_ENUM(GCFSValue_ValueType_OneOfCase) {
+ GCFSValue_ValueType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSValue_ValueType_OneOfCase_NullValue = 11,
+ GCFSValue_ValueType_OneOfCase_BooleanValue = 1,
+ GCFSValue_ValueType_OneOfCase_IntegerValue = 2,
+ GCFSValue_ValueType_OneOfCase_DoubleValue = 3,
+ GCFSValue_ValueType_OneOfCase_TimestampValue = 10,
+ GCFSValue_ValueType_OneOfCase_StringValue = 17,
+ GCFSValue_ValueType_OneOfCase_BytesValue = 18,
+ GCFSValue_ValueType_OneOfCase_ReferenceValue = 5,
+ GCFSValue_ValueType_OneOfCase_GeoPointValue = 8,
+ GCFSValue_ValueType_OneOfCase_ArrayValue = 9,
+ GCFSValue_ValueType_OneOfCase_MapValue = 6,
+};
+
+/**
+ * A message that can hold any of the supported value types.
+ **/
+@interface GCFSValue : GPBMessage
+
+/** Must have a value set. */
+@property(nonatomic, readonly) GCFSValue_ValueType_OneOfCase valueTypeOneOfCase;
+
+/** A null value. */
+@property(nonatomic, readwrite) enum GPBNullValue nullValue;
+
+/** A boolean value. */
+@property(nonatomic, readwrite) BOOL booleanValue;
+
+/** An integer value. */
+@property(nonatomic, readwrite) int64_t integerValue;
+
+/** A double value. */
+@property(nonatomic, readwrite) double doubleValue;
+
+/**
+ * A timestamp value.
+ *
+ * Precise only to microseconds. When stored, any additional precision is
+ * rounded down.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *timestampValue;
+
+/**
+ * A string value.
+ *
+ * The string, represented as UTF-8, must not exceed 1 MiB - 89 bytes.
+ * Only the first 1,500 bytes of the UTF-8 representation are considered by
+ * queries.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *stringValue;
+
+/**
+ * A bytes value.
+ *
+ * Must not exceed 1 MiB - 89 bytes.
+ * Only the first 1,500 bytes are considered by queries.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *bytesValue;
+
+/**
+ * A reference to a document. For example:
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *referenceValue;
+
+/** A geo point value representing a point on the surface of Earth. */
+@property(nonatomic, readwrite, strong, null_resettable) GTPLatLng *geoPointValue;
+
+/**
+ * An array value.
+ *
+ * Cannot contain another array value.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSArrayValue *arrayValue;
+
+/** A map value. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSMapValue *mapValue;
+
+@end
+
+/**
+ * Fetches the raw value of a @c GCFSValue's @c nullValue property, even
+ * if the value was not defined by the enum at the time the code was generated.
+ **/
+int32_t GCFSValue_NullValue_RawValue(GCFSValue *message);
+/**
+ * Sets the raw value of an @c GCFSValue's @c nullValue property, allowing
+ * it to be set to a value that was not defined by the enum at the time the code
+ * was generated.
+ **/
+void SetGCFSValue_NullValue_RawValue(GCFSValue *message, int32_t value);
+
+/**
+ * Clears whatever value was set for the oneof 'valueType'.
+ **/
+void GCFSValue_ClearValueTypeOneOfCase(GCFSValue *message);
+
+#pragma mark - GCFSArrayValue
+
+typedef GPB_ENUM(GCFSArrayValue_FieldNumber) {
+ GCFSArrayValue_FieldNumber_ValuesArray = 1,
+};
+
+/**
+ * An array value.
+ **/
+@interface GCFSArrayValue : GPBMessage
+
+/** Values in the array. */
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSValue*> *valuesArray;
+/** The number of items in @c valuesArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger valuesArray_Count;
+
+@end
+
+#pragma mark - GCFSMapValue
+
+typedef GPB_ENUM(GCFSMapValue_FieldNumber) {
+ GCFSMapValue_FieldNumber_Fields = 1,
+};
+
+/**
+ * A map value.
+ **/
+@interface GCFSMapValue : GPBMessage
+
+/**
+ * The map's fields.
+ *
+ * The map keys represent field names. Field names matching the regular
+ * expression `__.*__` are reserved. Reserved field names are forbidden except
+ * in certain documented contexts. The map keys, represented as UTF-8, must
+ * not exceed 1,500 bytes and cannot be empty.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableDictionary<NSString*, GCFSValue*> *fields;
+/** The number of items in @c fields without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger fields_Count;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.m
new file mode 100644
index 0000000..2c805c3
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.m
@@ -0,0 +1,412 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/document.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Struct.pbobjc.h>
+ #import <Protobuf/Timestamp.pbobjc.h>
+#else
+ #import "Struct.pbobjc.h"
+ #import "Timestamp.pbobjc.h"
+#endif
+
+ #import "Document.pbobjc.h"
+ #import "Annotations.pbobjc.h"
+ #import "Latlng.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#pragma clang diagnostic ignored "-Wdirect-ivar-access"
+
+#pragma mark - GCFSDocumentRoot
+
+@implementation GCFSDocumentRoot
+
+
+@end
+
+#pragma mark - GCFSDocumentRoot_FileDescriptor
+
+static GPBFileDescriptor *GCFSDocumentRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1"
+ objcPrefix:@"GCFS"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - GCFSDocument
+
+@implementation GCFSDocument
+
+@dynamic name;
+@dynamic fields, fields_Count;
+@dynamic hasCreateTime, createTime;
+@dynamic hasUpdateTime, updateTime;
+
+typedef struct GCFSDocument__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *name;
+ NSMutableDictionary *fields;
+ GPBTimestamp *createTime;
+ GPBTimestamp *updateTime;
+} GCFSDocument__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "name",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocument_FieldNumber_Name,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSDocument__storage_, name),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "fields",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue),
+ .number = GCFSDocument_FieldNumber_Fields,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSDocument__storage_, fields),
+ .flags = GPBFieldMapKeyString,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "createTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSDocument_FieldNumber_CreateTime,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSDocument__storage_, createTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "updateTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSDocument_FieldNumber_UpdateTime,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSDocument__storage_, updateTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSDocument class]
+ rootClass:[GCFSDocumentRoot class]
+ file:GCFSDocumentRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSDocument__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSValue
+
+@implementation GCFSValue
+
+@dynamic valueTypeOneOfCase;
+@dynamic nullValue;
+@dynamic booleanValue;
+@dynamic integerValue;
+@dynamic doubleValue;
+@dynamic timestampValue;
+@dynamic stringValue;
+@dynamic bytesValue;
+@dynamic referenceValue;
+@dynamic geoPointValue;
+@dynamic arrayValue;
+@dynamic mapValue;
+
+typedef struct GCFSValue__storage_ {
+ uint32_t _has_storage_[2];
+ GPBNullValue nullValue;
+ NSString *referenceValue;
+ GCFSMapValue *mapValue;
+ GTPLatLng *geoPointValue;
+ GCFSArrayValue *arrayValue;
+ GPBTimestamp *timestampValue;
+ NSString *stringValue;
+ NSData *bytesValue;
+ int64_t integerValue;
+ double doubleValue;
+} GCFSValue__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "booleanValue",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSValue_FieldNumber_BooleanValue,
+ .hasIndex = -1,
+ .offset = 0, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "integerValue",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSValue_FieldNumber_IntegerValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, integerValue),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt64,
+ },
+ {
+ .name = "doubleValue",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSValue_FieldNumber_DoubleValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, doubleValue),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeDouble,
+ },
+ {
+ .name = "referenceValue",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSValue_FieldNumber_ReferenceValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, referenceValue),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "mapValue",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSMapValue),
+ .number = GCFSValue_FieldNumber_MapValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, mapValue),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "geoPointValue",
+ .dataTypeSpecific.className = GPBStringifySymbol(GTPLatLng),
+ .number = GCFSValue_FieldNumber_GeoPointValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, geoPointValue),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "arrayValue",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSArrayValue),
+ .number = GCFSValue_FieldNumber_ArrayValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, arrayValue),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "timestampValue",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSValue_FieldNumber_TimestampValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, timestampValue),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "nullValue",
+ .dataTypeSpecific.enumDescFunc = GPBNullValue_EnumDescriptor,
+ .number = GCFSValue_FieldNumber_NullValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, nullValue),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "stringValue",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSValue_FieldNumber_StringValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, stringValue),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "bytesValue",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSValue_FieldNumber_BytesValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSValue__storage_, bytesValue),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSValue class]
+ rootClass:[GCFSDocumentRoot class]
+ file:GCFSDocumentRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSValue__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "valueType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+int32_t GCFSValue_NullValue_RawValue(GCFSValue *message) {
+ GPBDescriptor *descriptor = [GCFSValue descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSValue_FieldNumber_NullValue];
+ return GPBGetMessageInt32Field(message, field);
+}
+
+void SetGCFSValue_NullValue_RawValue(GCFSValue *message, int32_t value) {
+ GPBDescriptor *descriptor = [GCFSValue descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSValue_FieldNumber_NullValue];
+ GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax);
+}
+
+void GCFSValue_ClearValueTypeOneOfCase(GCFSValue *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSArrayValue
+
+@implementation GCFSArrayValue
+
+@dynamic valuesArray, valuesArray_Count;
+
+typedef struct GCFSArrayValue__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *valuesArray;
+} GCFSArrayValue__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "valuesArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue),
+ .number = GCFSArrayValue_FieldNumber_ValuesArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSArrayValue__storage_, valuesArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSArrayValue class]
+ rootClass:[GCFSDocumentRoot class]
+ file:GCFSDocumentRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSArrayValue__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSMapValue
+
+@implementation GCFSMapValue
+
+@dynamic fields, fields_Count;
+
+typedef struct GCFSMapValue__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableDictionary *fields;
+} GCFSMapValue__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "fields",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue),
+ .number = GCFSMapValue_FieldNumber_Fields,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSMapValue__storage_, fields),
+ .flags = GPBFieldMapKeyString,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSMapValue class]
+ rootClass:[GCFSDocumentRoot class]
+ file:GCFSDocumentRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSMapValue__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h
new file mode 100644
index 0000000..0acd8c0
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h
@@ -0,0 +1,1342 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/firestore.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GCFSDocument;
+@class GCFSDocumentChange;
+@class GCFSDocumentDelete;
+@class GCFSDocumentMask;
+@class GCFSDocumentRemove;
+@class GCFSExistenceFilter;
+@class GCFSPrecondition;
+@class GCFSStructuredQuery;
+@class GCFSTarget;
+@class GCFSTargetChange;
+@class GCFSTarget_DocumentsTarget;
+@class GCFSTarget_QueryTarget;
+@class GCFSTransactionOptions;
+@class GCFSWrite;
+@class GCFSWriteResult;
+@class GPBTimestamp;
+@class RPCStatus;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - Enum GCFSTargetChange_TargetChangeType
+
+/** The type of change. */
+typedef GPB_ENUM(GCFSTargetChange_TargetChangeType) {
+ /**
+ * Value used if any message's field encounters a value that is not defined
+ * by this enum. The message will also have C functions to get/set the rawValue
+ * of the field.
+ **/
+ GCFSTargetChange_TargetChangeType_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue,
+ /** No change has occurred. Used only to send an updated `resume_token`. */
+ GCFSTargetChange_TargetChangeType_NoChange = 0,
+
+ /** The targets have been added. */
+ GCFSTargetChange_TargetChangeType_Add = 1,
+
+ /** The targets have been removed. */
+ GCFSTargetChange_TargetChangeType_Remove = 2,
+
+ /**
+ * The targets reflect all changes committed before the targets were added
+ * to the stream.
+ *
+ * This will be sent after or with a `read_time` that is greater than or
+ * equal to the time at which the targets were added.
+ *
+ * Listeners can wait for this change if read-after-write semantics
+ * are desired.
+ **/
+ GCFSTargetChange_TargetChangeType_Current = 3,
+
+ /**
+ * The targets have been reset, and a new initial state for the targets
+ * will be returned in subsequent changes.
+ *
+ * After the initial state is complete, `CURRENT` will be returned even
+ * if the target was previously indicated to be `CURRENT`.
+ **/
+ GCFSTargetChange_TargetChangeType_Reset = 4,
+};
+
+GPBEnumDescriptor *GCFSTargetChange_TargetChangeType_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GCFSTargetChange_TargetChangeType_IsValidValue(int32_t value);
+
+#pragma mark - GCFSFirestoreRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GCFSFirestoreRoot : GPBRootObject
+@end
+
+#pragma mark - GCFSGetDocumentRequest
+
+typedef GPB_ENUM(GCFSGetDocumentRequest_FieldNumber) {
+ GCFSGetDocumentRequest_FieldNumber_Name = 1,
+ GCFSGetDocumentRequest_FieldNumber_Mask = 2,
+ GCFSGetDocumentRequest_FieldNumber_Transaction = 3,
+ GCFSGetDocumentRequest_FieldNumber_ReadTime = 5,
+};
+
+typedef GPB_ENUM(GCFSGetDocumentRequest_ConsistencySelector_OneOfCase) {
+ GCFSGetDocumentRequest_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSGetDocumentRequest_ConsistencySelector_OneOfCase_Transaction = 3,
+ GCFSGetDocumentRequest_ConsistencySelector_OneOfCase_ReadTime = 5,
+};
+
+/**
+ * The request for [Firestore.GetDocument][google.firestore.v1beta1.Firestore.GetDocument].
+ **/
+@interface GCFSGetDocumentRequest : GPBMessage
+
+/**
+ * The resource name of the Document to get. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *name;
+
+/**
+ * The fields to return. If not set, returns all fields.
+ *
+ * If the document has a field that is not present in this mask, that field
+ * will not be returned in the response.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask;
+/** Test to see if @c mask has been set. */
+@property(nonatomic, readwrite) BOOL hasMask;
+
+/**
+ * The consistency mode for this transaction.
+ * If not set, defaults to strong consistency.
+ **/
+@property(nonatomic, readonly) GCFSGetDocumentRequest_ConsistencySelector_OneOfCase consistencySelectorOneOfCase;
+
+/** Reads the document in a transaction. */
+@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction;
+
+/**
+ * Reads the version of the document at the given time.
+ * This may not be older than 60 seconds.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'consistencySelector'.
+ **/
+void GCFSGetDocumentRequest_ClearConsistencySelectorOneOfCase(GCFSGetDocumentRequest *message);
+
+#pragma mark - GCFSListDocumentsRequest
+
+typedef GPB_ENUM(GCFSListDocumentsRequest_FieldNumber) {
+ GCFSListDocumentsRequest_FieldNumber_Parent = 1,
+ GCFSListDocumentsRequest_FieldNumber_CollectionId = 2,
+ GCFSListDocumentsRequest_FieldNumber_PageSize = 3,
+ GCFSListDocumentsRequest_FieldNumber_PageToken = 4,
+ GCFSListDocumentsRequest_FieldNumber_OrderBy = 6,
+ GCFSListDocumentsRequest_FieldNumber_Mask = 7,
+ GCFSListDocumentsRequest_FieldNumber_Transaction = 8,
+ GCFSListDocumentsRequest_FieldNumber_ReadTime = 10,
+ GCFSListDocumentsRequest_FieldNumber_ShowMissing = 12,
+};
+
+typedef GPB_ENUM(GCFSListDocumentsRequest_ConsistencySelector_OneOfCase) {
+ GCFSListDocumentsRequest_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSListDocumentsRequest_ConsistencySelector_OneOfCase_Transaction = 8,
+ GCFSListDocumentsRequest_ConsistencySelector_OneOfCase_ReadTime = 10,
+};
+
+/**
+ * The request for [Firestore.ListDocuments][google.firestore.v1beta1.Firestore.ListDocuments].
+ **/
+@interface GCFSListDocumentsRequest : GPBMessage
+
+/**
+ * The parent resource name. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents` or
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ * For example:
+ * `projects/my-project/databases/my-database/documents` or
+ * `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom`
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *parent;
+
+/**
+ * The collection ID, relative to `parent`, to list. For example: `chatrooms`
+ * or `messages`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *collectionId;
+
+/** The maximum number of documents to return. */
+@property(nonatomic, readwrite) int32_t pageSize;
+
+/** The `next_page_token` value returned from a previous List request, if any. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *pageToken;
+
+/** The order to sort results by. For example: `priority desc, name`. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *orderBy;
+
+/**
+ * The fields to return. If not set, returns all fields.
+ *
+ * If a document has a field that is not present in this mask, that field
+ * will not be returned in the response.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask;
+/** Test to see if @c mask has been set. */
+@property(nonatomic, readwrite) BOOL hasMask;
+
+/**
+ * The consistency mode for this transaction.
+ * If not set, defaults to strong consistency.
+ **/
+@property(nonatomic, readonly) GCFSListDocumentsRequest_ConsistencySelector_OneOfCase consistencySelectorOneOfCase;
+
+/** Reads documents in a transaction. */
+@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction;
+
+/**
+ * Reads documents as they were at the given time.
+ * This may not be older than 60 seconds.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+
+/**
+ * If the list should show missing documents. A missing document is a
+ * document that does not exist but has sub-documents. These documents will
+ * be returned with a key but will not have fields, [Document.create_time][google.firestore.v1beta1.Document.create_time],
+ * or [Document.update_time][google.firestore.v1beta1.Document.update_time] set.
+ *
+ * Requests with `show_missing` may not specify `where` or
+ * `order_by`.
+ **/
+@property(nonatomic, readwrite) BOOL showMissing;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'consistencySelector'.
+ **/
+void GCFSListDocumentsRequest_ClearConsistencySelectorOneOfCase(GCFSListDocumentsRequest *message);
+
+#pragma mark - GCFSListDocumentsResponse
+
+typedef GPB_ENUM(GCFSListDocumentsResponse_FieldNumber) {
+ GCFSListDocumentsResponse_FieldNumber_DocumentsArray = 1,
+ GCFSListDocumentsResponse_FieldNumber_NextPageToken = 2,
+};
+
+/**
+ * The response for [Firestore.ListDocuments][google.firestore.v1beta1.Firestore.ListDocuments].
+ **/
+@interface GCFSListDocumentsResponse : GPBMessage
+
+/** The Documents found. */
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSDocument*> *documentsArray;
+/** The number of items in @c documentsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger documentsArray_Count;
+
+/** The next page token. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *nextPageToken;
+
+@end
+
+#pragma mark - GCFSCreateDocumentRequest
+
+typedef GPB_ENUM(GCFSCreateDocumentRequest_FieldNumber) {
+ GCFSCreateDocumentRequest_FieldNumber_Parent = 1,
+ GCFSCreateDocumentRequest_FieldNumber_CollectionId = 2,
+ GCFSCreateDocumentRequest_FieldNumber_DocumentId = 3,
+ GCFSCreateDocumentRequest_FieldNumber_Document = 4,
+ GCFSCreateDocumentRequest_FieldNumber_Mask = 5,
+};
+
+/**
+ * The request for [Firestore.CreateDocument][google.firestore.v1beta1.Firestore.CreateDocument].
+ **/
+@interface GCFSCreateDocumentRequest : GPBMessage
+
+/**
+ * The parent resource. For example:
+ * `projects/{project_id}/databases/{database_id}/documents` or
+ * `projects/{project_id}/databases/{database_id}/documents/chatrooms/{chatroom_id}`
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *parent;
+
+/** The collection ID, relative to `parent`, to list. For example: `chatrooms`. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *collectionId;
+
+/**
+ * The client-assigned document ID to use for this document.
+ *
+ * Optional. If not specified, an ID will be assigned by the service.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *documentId;
+
+/** The document to create. `name` must not be set. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document;
+/** Test to see if @c document has been set. */
+@property(nonatomic, readwrite) BOOL hasDocument;
+
+/**
+ * The fields to return. If not set, returns all fields.
+ *
+ * If the document has a field that is not present in this mask, that field
+ * will not be returned in the response.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask;
+/** Test to see if @c mask has been set. */
+@property(nonatomic, readwrite) BOOL hasMask;
+
+@end
+
+#pragma mark - GCFSUpdateDocumentRequest
+
+typedef GPB_ENUM(GCFSUpdateDocumentRequest_FieldNumber) {
+ GCFSUpdateDocumentRequest_FieldNumber_Document = 1,
+ GCFSUpdateDocumentRequest_FieldNumber_UpdateMask = 2,
+ GCFSUpdateDocumentRequest_FieldNumber_Mask = 3,
+ GCFSUpdateDocumentRequest_FieldNumber_CurrentDocument = 4,
+};
+
+/**
+ * The request for [Firestore.UpdateDocument][google.firestore.v1beta1.Firestore.UpdateDocument].
+ **/
+@interface GCFSUpdateDocumentRequest : GPBMessage
+
+/**
+ * The updated document.
+ * Creates the document if it does not already exist.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document;
+/** Test to see if @c document has been set. */
+@property(nonatomic, readwrite) BOOL hasDocument;
+
+/**
+ * The fields to update.
+ * None of the field paths in the mask may contain a reserved name.
+ *
+ * If the document exists on the server and has fields not referenced in the
+ * mask, they are left unchanged.
+ * Fields referenced in the mask, but not present in the input document, are
+ * deleted from the document on the server.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *updateMask;
+/** Test to see if @c updateMask has been set. */
+@property(nonatomic, readwrite) BOOL hasUpdateMask;
+
+/**
+ * The fields to return. If not set, returns all fields.
+ *
+ * If the document has a field that is not present in this mask, that field
+ * will not be returned in the response.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask;
+/** Test to see if @c mask has been set. */
+@property(nonatomic, readwrite) BOOL hasMask;
+
+/**
+ * An optional precondition on the document.
+ * The request will fail if this is set and not met by the target document.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSPrecondition *currentDocument;
+/** Test to see if @c currentDocument has been set. */
+@property(nonatomic, readwrite) BOOL hasCurrentDocument;
+
+@end
+
+#pragma mark - GCFSDeleteDocumentRequest
+
+typedef GPB_ENUM(GCFSDeleteDocumentRequest_FieldNumber) {
+ GCFSDeleteDocumentRequest_FieldNumber_Name = 1,
+ GCFSDeleteDocumentRequest_FieldNumber_CurrentDocument = 2,
+};
+
+/**
+ * The request for [Firestore.DeleteDocument][google.firestore.v1beta1.Firestore.DeleteDocument].
+ **/
+@interface GCFSDeleteDocumentRequest : GPBMessage
+
+/**
+ * The resource name of the Document to delete. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *name;
+
+/**
+ * An optional precondition on the document.
+ * The request will fail if this is set and not met by the target document.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSPrecondition *currentDocument;
+/** Test to see if @c currentDocument has been set. */
+@property(nonatomic, readwrite) BOOL hasCurrentDocument;
+
+@end
+
+#pragma mark - GCFSBatchGetDocumentsRequest
+
+typedef GPB_ENUM(GCFSBatchGetDocumentsRequest_FieldNumber) {
+ GCFSBatchGetDocumentsRequest_FieldNumber_Database = 1,
+ GCFSBatchGetDocumentsRequest_FieldNumber_DocumentsArray = 2,
+ GCFSBatchGetDocumentsRequest_FieldNumber_Mask = 3,
+ GCFSBatchGetDocumentsRequest_FieldNumber_Transaction = 4,
+ GCFSBatchGetDocumentsRequest_FieldNumber_NewTransaction = 5,
+ GCFSBatchGetDocumentsRequest_FieldNumber_ReadTime = 7,
+};
+
+typedef GPB_ENUM(GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase) {
+ GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase_Transaction = 4,
+ GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase_NewTransaction = 5,
+ GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase_ReadTime = 7,
+};
+
+/**
+ * The request for [Firestore.BatchGetDocuments][google.firestore.v1beta1.Firestore.BatchGetDocuments].
+ **/
+@interface GCFSBatchGetDocumentsRequest : GPBMessage
+
+/**
+ * The database name. In the format:
+ * `projects/{project_id}/databases/{database_id}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *database;
+
+/**
+ * The names of the documents to retrieve. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ * The request will fail if any of the document is not a child resource of the
+ * given `database`. Duplicate names will be elided.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *documentsArray;
+/** The number of items in @c documentsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger documentsArray_Count;
+
+/**
+ * The fields to return. If not set, returns all fields.
+ *
+ * If a document has a field that is not present in this mask, that field will
+ * not be returned in the response.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask;
+/** Test to see if @c mask has been set. */
+@property(nonatomic, readwrite) BOOL hasMask;
+
+/**
+ * The consistency mode for this transaction.
+ * If not set, defaults to strong consistency.
+ **/
+@property(nonatomic, readonly) GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase consistencySelectorOneOfCase;
+
+/** Reads documents in a transaction. */
+@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction;
+
+/**
+ * Starts a new transaction and reads the documents.
+ * Defaults to a read-only transaction.
+ * The new transaction ID will be returned as the first response in the
+ * stream.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions *newTransaction NS_RETURNS_NOT_RETAINED;
+
+/**
+ * Reads documents as they were at the given time.
+ * This may not be older than 60 seconds.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'consistencySelector'.
+ **/
+void GCFSBatchGetDocumentsRequest_ClearConsistencySelectorOneOfCase(GCFSBatchGetDocumentsRequest *message);
+
+#pragma mark - GCFSBatchGetDocumentsResponse
+
+typedef GPB_ENUM(GCFSBatchGetDocumentsResponse_FieldNumber) {
+ GCFSBatchGetDocumentsResponse_FieldNumber_Found = 1,
+ GCFSBatchGetDocumentsResponse_FieldNumber_Missing = 2,
+ GCFSBatchGetDocumentsResponse_FieldNumber_Transaction = 3,
+ GCFSBatchGetDocumentsResponse_FieldNumber_ReadTime = 4,
+};
+
+typedef GPB_ENUM(GCFSBatchGetDocumentsResponse_Result_OneOfCase) {
+ GCFSBatchGetDocumentsResponse_Result_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSBatchGetDocumentsResponse_Result_OneOfCase_Found = 1,
+ GCFSBatchGetDocumentsResponse_Result_OneOfCase_Missing = 2,
+};
+
+/**
+ * The streamed response for [Firestore.BatchGetDocuments][google.firestore.v1beta1.Firestore.BatchGetDocuments].
+ **/
+@interface GCFSBatchGetDocumentsResponse : GPBMessage
+
+/**
+ * A single result.
+ * This can be empty if the server is just returning a transaction.
+ **/
+@property(nonatomic, readonly) GCFSBatchGetDocumentsResponse_Result_OneOfCase resultOneOfCase;
+
+/** A document that was requested. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *found;
+
+/**
+ * A document name that was requested but does not exist. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *missing;
+
+/**
+ * The transaction that was started as part of this request.
+ * Will only be set in the first response, and only if
+ * [BatchGetDocumentsRequest.new_transaction][google.firestore.v1beta1.BatchGetDocumentsRequest.new_transaction] was set in the request.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction;
+
+/**
+ * The time at which the document was read.
+ * This may be monotically increasing, in this case the previous documents in
+ * the result stream are guaranteed not to have changed between their
+ * read_time and this one.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+/** Test to see if @c readTime has been set. */
+@property(nonatomic, readwrite) BOOL hasReadTime;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'result'.
+ **/
+void GCFSBatchGetDocumentsResponse_ClearResultOneOfCase(GCFSBatchGetDocumentsResponse *message);
+
+#pragma mark - GCFSBeginTransactionRequest
+
+typedef GPB_ENUM(GCFSBeginTransactionRequest_FieldNumber) {
+ GCFSBeginTransactionRequest_FieldNumber_Database = 1,
+ GCFSBeginTransactionRequest_FieldNumber_Options = 2,
+};
+
+/**
+ * The request for [Firestore.BeginTransaction][google.firestore.v1beta1.Firestore.BeginTransaction].
+ **/
+@interface GCFSBeginTransactionRequest : GPBMessage
+
+/**
+ * The database name. In the format:
+ * `projects/{project_id}/databases/{database_id}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *database;
+
+/**
+ * The options for the transaction.
+ * Defaults to a read-write transaction.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions *options;
+/** Test to see if @c options has been set. */
+@property(nonatomic, readwrite) BOOL hasOptions;
+
+@end
+
+#pragma mark - GCFSBeginTransactionResponse
+
+typedef GPB_ENUM(GCFSBeginTransactionResponse_FieldNumber) {
+ GCFSBeginTransactionResponse_FieldNumber_Transaction = 1,
+};
+
+/**
+ * The response for [Firestore.BeginTransaction][google.firestore.v1beta1.Firestore.BeginTransaction].
+ **/
+@interface GCFSBeginTransactionResponse : GPBMessage
+
+/** The transaction that was started. */
+@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction;
+
+@end
+
+#pragma mark - GCFSCommitRequest
+
+typedef GPB_ENUM(GCFSCommitRequest_FieldNumber) {
+ GCFSCommitRequest_FieldNumber_Database = 1,
+ GCFSCommitRequest_FieldNumber_WritesArray = 2,
+ GCFSCommitRequest_FieldNumber_Transaction = 3,
+};
+
+/**
+ * The request for [Firestore.Commit][google.firestore.v1beta1.Firestore.Commit].
+ **/
+@interface GCFSCommitRequest : GPBMessage
+
+/**
+ * The database name. In the format:
+ * `projects/{project_id}/databases/{database_id}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *database;
+
+/**
+ * The writes to apply.
+ *
+ * Always executed atomically and in order.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSWrite*> *writesArray;
+/** The number of items in @c writesArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger writesArray_Count;
+
+/**
+ * If non-empty, applies all writes in this transaction, and commits it.
+ * Otherwise, applies the writes as if they were in their own transaction.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction;
+
+@end
+
+#pragma mark - GCFSCommitResponse
+
+typedef GPB_ENUM(GCFSCommitResponse_FieldNumber) {
+ GCFSCommitResponse_FieldNumber_WriteResultsArray = 1,
+ GCFSCommitResponse_FieldNumber_CommitTime = 2,
+};
+
+/**
+ * The response for [Firestore.Commit][google.firestore.v1beta1.Firestore.Commit].
+ **/
+@interface GCFSCommitResponse : GPBMessage
+
+/**
+ * The result of applying the writes.
+ *
+ * This i-th write result corresponds to the i-th write in the
+ * request.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSWriteResult*> *writeResultsArray;
+/** The number of items in @c writeResultsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger writeResultsArray_Count;
+
+/** The time at which the commit occurred. */
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *commitTime;
+/** Test to see if @c commitTime has been set. */
+@property(nonatomic, readwrite) BOOL hasCommitTime;
+
+@end
+
+#pragma mark - GCFSRollbackRequest
+
+typedef GPB_ENUM(GCFSRollbackRequest_FieldNumber) {
+ GCFSRollbackRequest_FieldNumber_Database = 1,
+ GCFSRollbackRequest_FieldNumber_Transaction = 2,
+};
+
+/**
+ * The request for [Firestore.Rollback][google.firestore.v1beta1.Firestore.Rollback].
+ **/
+@interface GCFSRollbackRequest : GPBMessage
+
+/**
+ * The database name. In the format:
+ * `projects/{project_id}/databases/{database_id}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *database;
+
+/** The transaction to roll back. */
+@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction;
+
+@end
+
+#pragma mark - GCFSRunQueryRequest
+
+typedef GPB_ENUM(GCFSRunQueryRequest_FieldNumber) {
+ GCFSRunQueryRequest_FieldNumber_Parent = 1,
+ GCFSRunQueryRequest_FieldNumber_StructuredQuery = 2,
+ GCFSRunQueryRequest_FieldNumber_Transaction = 5,
+ GCFSRunQueryRequest_FieldNumber_NewTransaction = 6,
+ GCFSRunQueryRequest_FieldNumber_ReadTime = 7,
+};
+
+typedef GPB_ENUM(GCFSRunQueryRequest_QueryType_OneOfCase) {
+ GCFSRunQueryRequest_QueryType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSRunQueryRequest_QueryType_OneOfCase_StructuredQuery = 2,
+};
+
+typedef GPB_ENUM(GCFSRunQueryRequest_ConsistencySelector_OneOfCase) {
+ GCFSRunQueryRequest_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSRunQueryRequest_ConsistencySelector_OneOfCase_Transaction = 5,
+ GCFSRunQueryRequest_ConsistencySelector_OneOfCase_NewTransaction = 6,
+ GCFSRunQueryRequest_ConsistencySelector_OneOfCase_ReadTime = 7,
+};
+
+/**
+ * The request for [Firestore.RunQuery][google.firestore.v1beta1.Firestore.RunQuery].
+ **/
+@interface GCFSRunQueryRequest : GPBMessage
+
+/**
+ * The parent resource name. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents` or
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ * For example:
+ * `projects/my-project/databases/my-database/documents` or
+ * `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom`
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *parent;
+
+/** The query to run. */
+@property(nonatomic, readonly) GCFSRunQueryRequest_QueryType_OneOfCase queryTypeOneOfCase;
+
+/** A structured query. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery *structuredQuery;
+
+/**
+ * The consistency mode for this transaction.
+ * If not set, defaults to strong consistency.
+ **/
+@property(nonatomic, readonly) GCFSRunQueryRequest_ConsistencySelector_OneOfCase consistencySelectorOneOfCase;
+
+/** Reads documents in a transaction. */
+@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction;
+
+/**
+ * Starts a new transaction and reads the documents.
+ * Defaults to a read-only transaction.
+ * The new transaction ID will be returned as the first response in the
+ * stream.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions *newTransaction NS_RETURNS_NOT_RETAINED;
+
+/**
+ * Reads documents as they were at the given time.
+ * This may not be older than 60 seconds.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'queryType'.
+ **/
+void GCFSRunQueryRequest_ClearQueryTypeOneOfCase(GCFSRunQueryRequest *message);
+/**
+ * Clears whatever value was set for the oneof 'consistencySelector'.
+ **/
+void GCFSRunQueryRequest_ClearConsistencySelectorOneOfCase(GCFSRunQueryRequest *message);
+
+#pragma mark - GCFSRunQueryResponse
+
+typedef GPB_ENUM(GCFSRunQueryResponse_FieldNumber) {
+ GCFSRunQueryResponse_FieldNumber_Document = 1,
+ GCFSRunQueryResponse_FieldNumber_Transaction = 2,
+ GCFSRunQueryResponse_FieldNumber_ReadTime = 3,
+ GCFSRunQueryResponse_FieldNumber_SkippedResults = 4,
+};
+
+/**
+ * The response for [Firestore.RunQuery][google.firestore.v1beta1.Firestore.RunQuery].
+ **/
+@interface GCFSRunQueryResponse : GPBMessage
+
+/**
+ * The transaction that was started as part of this request.
+ * Can only be set in the first response, and only if
+ * [RunQueryRequest.new_transaction][google.firestore.v1beta1.RunQueryRequest.new_transaction] was set in the request.
+ * If set, no other fields will be set in this response.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction;
+
+/**
+ * A query result.
+ * Not set when reporting partial progress.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document;
+/** Test to see if @c document has been set. */
+@property(nonatomic, readwrite) BOOL hasDocument;
+
+/**
+ * The time at which the document was read. This may be monotonically
+ * increasing; in this case, the previous documents in the result stream are
+ * guaranteed not to have changed between their `read_time` and this one.
+ *
+ * If the query returns no results, a response with `read_time` and no
+ * `document` will be sent, and this represents the time at which the query
+ * was run.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+/** Test to see if @c readTime has been set. */
+@property(nonatomic, readwrite) BOOL hasReadTime;
+
+/**
+ * The number of results that have been skipped due to an offset between
+ * the last response and the current response.
+ **/
+@property(nonatomic, readwrite) int32_t skippedResults;
+
+@end
+
+#pragma mark - GCFSWriteRequest
+
+typedef GPB_ENUM(GCFSWriteRequest_FieldNumber) {
+ GCFSWriteRequest_FieldNumber_Database = 1,
+ GCFSWriteRequest_FieldNumber_StreamId = 2,
+ GCFSWriteRequest_FieldNumber_WritesArray = 3,
+ GCFSWriteRequest_FieldNumber_StreamToken = 4,
+ GCFSWriteRequest_FieldNumber_Labels = 5,
+};
+
+/**
+ * The request for [Firestore.Write][google.firestore.v1beta1.Firestore.Write].
+ *
+ * The first request creates a stream, or resumes an existing one from a token.
+ *
+ * When creating a new stream, the server replies with a response containing
+ * only an ID and a token, to use in the next request.
+ *
+ * When resuming a stream, the server first streams any responses later than the
+ * given token, then a response containing only an up-to-date token, to use in
+ * the next request.
+ **/
+@interface GCFSWriteRequest : GPBMessage
+
+/**
+ * The database name. In the format:
+ * `projects/{project_id}/databases/{database_id}`.
+ * This is only required in the first message.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *database;
+
+/**
+ * The ID of the write stream to resume.
+ * This may only be set in the first message. When left empty, a new write
+ * stream will be created.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *streamId;
+
+/**
+ * The writes to apply.
+ *
+ * Always executed atomically and in order.
+ * This must be empty on the first request.
+ * This may be empty on the last request.
+ * This must not be empty on all other requests.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSWrite*> *writesArray;
+/** The number of items in @c writesArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger writesArray_Count;
+
+/**
+ * A stream token that was previously sent by the server.
+ *
+ * The client should set this field to the token from the most recent
+ * [WriteResponse][google.firestore.v1beta1.WriteResponse] it has received. This acknowledges that the client has
+ * received responses up to this token. After sending this token, earlier
+ * tokens may not be used anymore.
+ *
+ * The server may close the stream if there are too many unacknowledged
+ * responses.
+ *
+ * Leave this field unset when creating a new stream. To resume a stream at
+ * a specific point, set this field and the `stream_id` field.
+ *
+ * Leave this field unset when creating a new stream.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *streamToken;
+
+/** Labels associated with this write request. */
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableDictionary<NSString*, NSString*> *labels;
+/** The number of items in @c labels without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger labels_Count;
+
+@end
+
+#pragma mark - GCFSWriteResponse
+
+typedef GPB_ENUM(GCFSWriteResponse_FieldNumber) {
+ GCFSWriteResponse_FieldNumber_StreamId = 1,
+ GCFSWriteResponse_FieldNumber_StreamToken = 2,
+ GCFSWriteResponse_FieldNumber_WriteResultsArray = 3,
+ GCFSWriteResponse_FieldNumber_CommitTime = 4,
+};
+
+/**
+ * The response for [Firestore.Write][google.firestore.v1beta1.Firestore.Write].
+ **/
+@interface GCFSWriteResponse : GPBMessage
+
+/**
+ * The ID of the stream.
+ * Only set on the first message, when a new stream was created.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *streamId;
+
+/**
+ * A token that represents the position of this response in the stream.
+ * This can be used by a client to resume the stream at this point.
+ *
+ * This field is always set.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *streamToken;
+
+/**
+ * The result of applying the writes.
+ *
+ * This i-th write result corresponds to the i-th write in the
+ * request.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSWriteResult*> *writeResultsArray;
+/** The number of items in @c writeResultsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger writeResultsArray_Count;
+
+/** The time at which the commit occurred. */
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *commitTime;
+/** Test to see if @c commitTime has been set. */
+@property(nonatomic, readwrite) BOOL hasCommitTime;
+
+@end
+
+#pragma mark - GCFSListenRequest
+
+typedef GPB_ENUM(GCFSListenRequest_FieldNumber) {
+ GCFSListenRequest_FieldNumber_Database = 1,
+ GCFSListenRequest_FieldNumber_AddTarget = 2,
+ GCFSListenRequest_FieldNumber_RemoveTarget = 3,
+ GCFSListenRequest_FieldNumber_Labels = 4,
+};
+
+typedef GPB_ENUM(GCFSListenRequest_TargetChange_OneOfCase) {
+ GCFSListenRequest_TargetChange_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSListenRequest_TargetChange_OneOfCase_AddTarget = 2,
+ GCFSListenRequest_TargetChange_OneOfCase_RemoveTarget = 3,
+};
+
+/**
+ * A request for [Firestore.Listen][google.firestore.v1beta1.Firestore.Listen]
+ **/
+@interface GCFSListenRequest : GPBMessage
+
+/**
+ * The database name. In the format:
+ * `projects/{project_id}/databases/{database_id}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *database;
+
+/** The supported target changes. */
+@property(nonatomic, readonly) GCFSListenRequest_TargetChange_OneOfCase targetChangeOneOfCase;
+
+/** A target to add to this stream. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget *addTarget;
+
+/** The ID of a target to remove from this stream. */
+@property(nonatomic, readwrite) int32_t removeTarget;
+
+/** Labels associated with this target change. */
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableDictionary<NSString*, NSString*> *labels;
+/** The number of items in @c labels without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger labels_Count;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'targetChange'.
+ **/
+void GCFSListenRequest_ClearTargetChangeOneOfCase(GCFSListenRequest *message);
+
+#pragma mark - GCFSListenResponse
+
+typedef GPB_ENUM(GCFSListenResponse_FieldNumber) {
+ GCFSListenResponse_FieldNumber_TargetChange = 2,
+ GCFSListenResponse_FieldNumber_DocumentChange = 3,
+ GCFSListenResponse_FieldNumber_DocumentDelete = 4,
+ GCFSListenResponse_FieldNumber_Filter = 5,
+ GCFSListenResponse_FieldNumber_DocumentRemove = 6,
+};
+
+typedef GPB_ENUM(GCFSListenResponse_ResponseType_OneOfCase) {
+ GCFSListenResponse_ResponseType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSListenResponse_ResponseType_OneOfCase_TargetChange = 2,
+ GCFSListenResponse_ResponseType_OneOfCase_DocumentChange = 3,
+ GCFSListenResponse_ResponseType_OneOfCase_DocumentDelete = 4,
+ GCFSListenResponse_ResponseType_OneOfCase_DocumentRemove = 6,
+ GCFSListenResponse_ResponseType_OneOfCase_Filter = 5,
+};
+
+/**
+ * The response for [Firestore.Listen][google.firestore.v1beta1.Firestore.Listen].
+ **/
+@interface GCFSListenResponse : GPBMessage
+
+/** The supported responses. */
+@property(nonatomic, readonly) GCFSListenResponse_ResponseType_OneOfCase responseTypeOneOfCase;
+
+/** Targets have changed. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTargetChange *targetChange;
+
+/** A [Document][google.firestore.v1beta1.Document] has changed. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentChange *documentChange;
+
+/** A [Document][google.firestore.v1beta1.Document] has been deleted. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentDelete *documentDelete;
+
+/**
+ * A [Document][google.firestore.v1beta1.Document] has been removed from a target (because it is no longer
+ * relevant to that target).
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentRemove *documentRemove;
+
+/**
+ * A filter to apply to the set of documents previously returned for the
+ * given target.
+ *
+ * Returned when documents may have been removed from the given target, but
+ * the exact documents are unknown.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSExistenceFilter *filter;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'responseType'.
+ **/
+void GCFSListenResponse_ClearResponseTypeOneOfCase(GCFSListenResponse *message);
+
+#pragma mark - GCFSTarget
+
+typedef GPB_ENUM(GCFSTarget_FieldNumber) {
+ GCFSTarget_FieldNumber_Query = 2,
+ GCFSTarget_FieldNumber_Documents = 3,
+ GCFSTarget_FieldNumber_ResumeToken = 4,
+ GCFSTarget_FieldNumber_TargetId = 5,
+ GCFSTarget_FieldNumber_Once = 6,
+ GCFSTarget_FieldNumber_ReadTime = 11,
+};
+
+typedef GPB_ENUM(GCFSTarget_TargetType_OneOfCase) {
+ GCFSTarget_TargetType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSTarget_TargetType_OneOfCase_Query = 2,
+ GCFSTarget_TargetType_OneOfCase_Documents = 3,
+};
+
+typedef GPB_ENUM(GCFSTarget_ResumeType_OneOfCase) {
+ GCFSTarget_ResumeType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSTarget_ResumeType_OneOfCase_ResumeToken = 4,
+ GCFSTarget_ResumeType_OneOfCase_ReadTime = 11,
+};
+
+/**
+ * A specification of a set of documents to listen to.
+ **/
+@interface GCFSTarget : GPBMessage
+
+/** The type of target to listen to. */
+@property(nonatomic, readonly) GCFSTarget_TargetType_OneOfCase targetTypeOneOfCase;
+
+/** A target specified by a query. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget_QueryTarget *query;
+
+/** A target specified by a set of document names. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget_DocumentsTarget *documents;
+
+/**
+ * When to start listening.
+ *
+ * If not specified, all matching Documents are returned before any
+ * subsequent changes.
+ **/
+@property(nonatomic, readonly) GCFSTarget_ResumeType_OneOfCase resumeTypeOneOfCase;
+
+/**
+ * A resume token from a prior [TargetChange][google.firestore.v1beta1.TargetChange] for an identical target.
+ *
+ * Using a resume token with a different target is unsupported and may fail.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *resumeToken;
+
+/**
+ * Start listening after a specific `read_time`.
+ *
+ * The client must know the state of matching documents at this time.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+
+/**
+ * A client provided target ID.
+ *
+ * If not set, the server will assign an ID for the target.
+ *
+ * Used for resuming a target without changing IDs. The IDs can either be
+ * client-assigned or be server-assigned in a previous stream. All targets
+ * with client provided IDs must be added before adding a target that needs
+ * a server-assigned id.
+ **/
+@property(nonatomic, readwrite) int32_t targetId;
+
+/** If the target should be removed once it is current and consistent. */
+@property(nonatomic, readwrite) BOOL once;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'targetType'.
+ **/
+void GCFSTarget_ClearTargetTypeOneOfCase(GCFSTarget *message);
+/**
+ * Clears whatever value was set for the oneof 'resumeType'.
+ **/
+void GCFSTarget_ClearResumeTypeOneOfCase(GCFSTarget *message);
+
+#pragma mark - GCFSTarget_DocumentsTarget
+
+typedef GPB_ENUM(GCFSTarget_DocumentsTarget_FieldNumber) {
+ GCFSTarget_DocumentsTarget_FieldNumber_DocumentsArray = 2,
+};
+
+/**
+ * A target specified by a set of documents names.
+ **/
+@interface GCFSTarget_DocumentsTarget : GPBMessage
+
+/**
+ * The names of the documents to retrieve. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ * The request will fail if any of the document is not a child resource of
+ * the given `database`. Duplicate names will be elided.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *documentsArray;
+/** The number of items in @c documentsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger documentsArray_Count;
+
+@end
+
+#pragma mark - GCFSTarget_QueryTarget
+
+typedef GPB_ENUM(GCFSTarget_QueryTarget_FieldNumber) {
+ GCFSTarget_QueryTarget_FieldNumber_Parent = 1,
+ GCFSTarget_QueryTarget_FieldNumber_StructuredQuery = 2,
+};
+
+typedef GPB_ENUM(GCFSTarget_QueryTarget_QueryType_OneOfCase) {
+ GCFSTarget_QueryTarget_QueryType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSTarget_QueryTarget_QueryType_OneOfCase_StructuredQuery = 2,
+};
+
+/**
+ * A target specified by a query.
+ **/
+@interface GCFSTarget_QueryTarget : GPBMessage
+
+/**
+ * The parent resource name. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents` or
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ * For example:
+ * `projects/my-project/databases/my-database/documents` or
+ * `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom`
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *parent;
+
+/** The query to run. */
+@property(nonatomic, readonly) GCFSTarget_QueryTarget_QueryType_OneOfCase queryTypeOneOfCase;
+
+/** A structured query. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery *structuredQuery;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'queryType'.
+ **/
+void GCFSTarget_QueryTarget_ClearQueryTypeOneOfCase(GCFSTarget_QueryTarget *message);
+
+#pragma mark - GCFSTargetChange
+
+typedef GPB_ENUM(GCFSTargetChange_FieldNumber) {
+ GCFSTargetChange_FieldNumber_TargetChangeType = 1,
+ GCFSTargetChange_FieldNumber_TargetIdsArray = 2,
+ GCFSTargetChange_FieldNumber_Cause = 3,
+ GCFSTargetChange_FieldNumber_ResumeToken = 4,
+ GCFSTargetChange_FieldNumber_ReadTime = 6,
+};
+
+/**
+ * Targets being watched have changed.
+ **/
+@interface GCFSTargetChange : GPBMessage
+
+/** The type of change that occurred. */
+@property(nonatomic, readwrite) GCFSTargetChange_TargetChangeType targetChangeType;
+
+/**
+ * The target IDs of targets that have changed.
+ *
+ * If empty, the change applies to all targets.
+ *
+ * For `target_change_type=ADD`, the order of the target IDs matches the order
+ * of the requests to add the targets. This allows clients to unambiguously
+ * associate server-assigned target IDs with added targets.
+ *
+ * For other states, the order of the target IDs is not defined.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *targetIdsArray;
+/** The number of items in @c targetIdsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger targetIdsArray_Count;
+
+/** The error that resulted in this change, if applicable. */
+@property(nonatomic, readwrite, strong, null_resettable) RPCStatus *cause;
+/** Test to see if @c cause has been set. */
+@property(nonatomic, readwrite) BOOL hasCause;
+
+/**
+ * A token that can be used to resume the stream for the given `target_ids`,
+ * or all targets if `target_ids` is empty.
+ *
+ * Not set on every target change.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSData *resumeToken;
+
+/**
+ * The consistent `read_time` for the given `target_ids` (omitted when the
+ * target_ids are not at a consistent snapshot).
+ *
+ * The stream is guaranteed to send a `read_time` with `target_ids` empty
+ * whenever the entire stream reaches a new consistent snapshot. ADD,
+ * CURRENT, and RESET messages are guaranteed to (eventually) result in a
+ * new consistent snapshot (while NO_CHANGE and REMOVE messages are not).
+ *
+ * For a given stream, `read_time` is guaranteed to be monotonically
+ * increasing.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+/** Test to see if @c readTime has been set. */
+@property(nonatomic, readwrite) BOOL hasReadTime;
+
+@end
+
+/**
+ * Fetches the raw value of a @c GCFSTargetChange's @c targetChangeType property, even
+ * if the value was not defined by the enum at the time the code was generated.
+ **/
+int32_t GCFSTargetChange_TargetChangeType_RawValue(GCFSTargetChange *message);
+/**
+ * Sets the raw value of an @c GCFSTargetChange's @c targetChangeType property, allowing
+ * it to be set to a value that was not defined by the enum at the time the code
+ * was generated.
+ **/
+void SetGCFSTargetChange_TargetChangeType_RawValue(GCFSTargetChange *message, int32_t value);
+
+#pragma mark - GCFSListCollectionIdsRequest
+
+typedef GPB_ENUM(GCFSListCollectionIdsRequest_FieldNumber) {
+ GCFSListCollectionIdsRequest_FieldNumber_Parent = 1,
+ GCFSListCollectionIdsRequest_FieldNumber_PageSize = 2,
+ GCFSListCollectionIdsRequest_FieldNumber_PageToken = 3,
+};
+
+/**
+ * The request for [Firestore.ListCollectionIds][google.firestore.v1beta1.Firestore.ListCollectionIds].
+ **/
+@interface GCFSListCollectionIdsRequest : GPBMessage
+
+/**
+ * The parent document. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ * For example:
+ * `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom`
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *parent;
+
+/** The maximum number of results to return. */
+@property(nonatomic, readwrite) int32_t pageSize;
+
+/**
+ * A page token. Must be a value from
+ * [ListCollectionIdsResponse][google.firestore.v1beta1.ListCollectionIdsResponse].
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *pageToken;
+
+@end
+
+#pragma mark - GCFSListCollectionIdsResponse
+
+typedef GPB_ENUM(GCFSListCollectionIdsResponse_FieldNumber) {
+ GCFSListCollectionIdsResponse_FieldNumber_CollectionIdsArray = 1,
+ GCFSListCollectionIdsResponse_FieldNumber_NextPageToken = 2,
+};
+
+/**
+ * The response from [Firestore.ListCollectionIds][google.firestore.v1beta1.Firestore.ListCollectionIds].
+ **/
+@interface GCFSListCollectionIdsResponse : GPBMessage
+
+/** The collection ids. */
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<NSString*> *collectionIdsArray;
+/** The number of items in @c collectionIdsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger collectionIdsArray_Count;
+
+/** A page token that may be used to continue the list. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *nextPageToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.m
new file mode 100644
index 0000000..4bdee01
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.m
@@ -0,0 +1,2064 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/firestore.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Empty.pbobjc.h>
+ #import <Protobuf/Timestamp.pbobjc.h>
+#else
+ #import "Empty.pbobjc.h"
+ #import "Timestamp.pbobjc.h"
+#endif
+
+ #import "Firestore.pbobjc.h"
+ #import "Annotations.pbobjc.h"
+ #import "Common.pbobjc.h"
+ #import "Document.pbobjc.h"
+ #import "Query.pbobjc.h"
+ #import "Write.pbobjc.h"
+ #import "Status.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#pragma clang diagnostic ignored "-Wdirect-ivar-access"
+
+#pragma mark - GCFSFirestoreRoot
+
+@implementation GCFSFirestoreRoot
+
+
+@end
+
+#pragma mark - GCFSFirestoreRoot_FileDescriptor
+
+static GPBFileDescriptor *GCFSFirestoreRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1"
+ objcPrefix:@"GCFS"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - GCFSGetDocumentRequest
+
+@implementation GCFSGetDocumentRequest
+
+@dynamic consistencySelectorOneOfCase;
+@dynamic name;
+@dynamic hasMask, mask;
+@dynamic transaction;
+@dynamic readTime;
+
+typedef struct GCFSGetDocumentRequest__storage_ {
+ uint32_t _has_storage_[2];
+ NSString *name;
+ GCFSDocumentMask *mask;
+ NSData *transaction;
+ GPBTimestamp *readTime;
+} GCFSGetDocumentRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "name",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSGetDocumentRequest_FieldNumber_Name,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSGetDocumentRequest__storage_, name),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "mask",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask),
+ .number = GCFSGetDocumentRequest_FieldNumber_Mask,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSGetDocumentRequest__storage_, mask),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "transaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSGetDocumentRequest_FieldNumber_Transaction,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSGetDocumentRequest__storage_, transaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSGetDocumentRequest_FieldNumber_ReadTime,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSGetDocumentRequest__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSGetDocumentRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSGetDocumentRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "consistencySelector",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSGetDocumentRequest_ClearConsistencySelectorOneOfCase(GCFSGetDocumentRequest *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSListDocumentsRequest
+
+@implementation GCFSListDocumentsRequest
+
+@dynamic consistencySelectorOneOfCase;
+@dynamic parent;
+@dynamic collectionId;
+@dynamic pageSize;
+@dynamic pageToken;
+@dynamic orderBy;
+@dynamic hasMask, mask;
+@dynamic transaction;
+@dynamic readTime;
+@dynamic showMissing;
+
+typedef struct GCFSListDocumentsRequest__storage_ {
+ uint32_t _has_storage_[2];
+ int32_t pageSize;
+ NSString *parent;
+ NSString *collectionId;
+ NSString *pageToken;
+ NSString *orderBy;
+ GCFSDocumentMask *mask;
+ NSData *transaction;
+ GPBTimestamp *readTime;
+} GCFSListDocumentsRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "parent",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListDocumentsRequest_FieldNumber_Parent,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, parent),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "collectionId",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListDocumentsRequest_FieldNumber_CollectionId,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, collectionId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "pageSize",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListDocumentsRequest_FieldNumber_PageSize,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, pageSize),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "pageToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListDocumentsRequest_FieldNumber_PageToken,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, pageToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "orderBy",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListDocumentsRequest_FieldNumber_OrderBy,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, orderBy),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "mask",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask),
+ .number = GCFSListDocumentsRequest_FieldNumber_Mask,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, mask),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "transaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListDocumentsRequest_FieldNumber_Transaction,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, transaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSListDocumentsRequest_FieldNumber_ReadTime,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "showMissing",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListDocumentsRequest_FieldNumber_ShowMissing,
+ .hasIndex = 6,
+ .offset = 7, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSListDocumentsRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSListDocumentsRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "consistencySelector",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSListDocumentsRequest_ClearConsistencySelectorOneOfCase(GCFSListDocumentsRequest *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSListDocumentsResponse
+
+@implementation GCFSListDocumentsResponse
+
+@dynamic documentsArray, documentsArray_Count;
+@dynamic nextPageToken;
+
+typedef struct GCFSListDocumentsResponse__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *documentsArray;
+ NSString *nextPageToken;
+} GCFSListDocumentsResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "documentsArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument),
+ .number = GCFSListDocumentsResponse_FieldNumber_DocumentsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsResponse__storage_, documentsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "nextPageToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListDocumentsResponse_FieldNumber_NextPageToken,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSListDocumentsResponse__storage_, nextPageToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSListDocumentsResponse class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSListDocumentsResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSCreateDocumentRequest
+
+@implementation GCFSCreateDocumentRequest
+
+@dynamic parent;
+@dynamic collectionId;
+@dynamic documentId;
+@dynamic hasDocument, document;
+@dynamic hasMask, mask;
+
+typedef struct GCFSCreateDocumentRequest__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *parent;
+ NSString *collectionId;
+ NSString *documentId;
+ GCFSDocument *document;
+ GCFSDocumentMask *mask;
+} GCFSCreateDocumentRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "parent",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSCreateDocumentRequest_FieldNumber_Parent,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, parent),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "collectionId",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSCreateDocumentRequest_FieldNumber_CollectionId,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, collectionId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "documentId",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSCreateDocumentRequest_FieldNumber_DocumentId,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, documentId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "document",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument),
+ .number = GCFSCreateDocumentRequest_FieldNumber_Document,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, document),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "mask",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask),
+ .number = GCFSCreateDocumentRequest_FieldNumber_Mask,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, mask),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSCreateDocumentRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSCreateDocumentRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSUpdateDocumentRequest
+
+@implementation GCFSUpdateDocumentRequest
+
+@dynamic hasDocument, document;
+@dynamic hasUpdateMask, updateMask;
+@dynamic hasMask, mask;
+@dynamic hasCurrentDocument, currentDocument;
+
+typedef struct GCFSUpdateDocumentRequest__storage_ {
+ uint32_t _has_storage_[1];
+ GCFSDocument *document;
+ GCFSDocumentMask *updateMask;
+ GCFSDocumentMask *mask;
+ GCFSPrecondition *currentDocument;
+} GCFSUpdateDocumentRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "document",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument),
+ .number = GCFSUpdateDocumentRequest_FieldNumber_Document,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSUpdateDocumentRequest__storage_, document),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "updateMask",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask),
+ .number = GCFSUpdateDocumentRequest_FieldNumber_UpdateMask,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSUpdateDocumentRequest__storage_, updateMask),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "mask",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask),
+ .number = GCFSUpdateDocumentRequest_FieldNumber_Mask,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSUpdateDocumentRequest__storage_, mask),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "currentDocument",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSPrecondition),
+ .number = GCFSUpdateDocumentRequest_FieldNumber_CurrentDocument,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GCFSUpdateDocumentRequest__storage_, currentDocument),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSUpdateDocumentRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSUpdateDocumentRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSDeleteDocumentRequest
+
+@implementation GCFSDeleteDocumentRequest
+
+@dynamic name;
+@dynamic hasCurrentDocument, currentDocument;
+
+typedef struct GCFSDeleteDocumentRequest__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *name;
+ GCFSPrecondition *currentDocument;
+} GCFSDeleteDocumentRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "name",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDeleteDocumentRequest_FieldNumber_Name,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSDeleteDocumentRequest__storage_, name),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "currentDocument",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSPrecondition),
+ .number = GCFSDeleteDocumentRequest_FieldNumber_CurrentDocument,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSDeleteDocumentRequest__storage_, currentDocument),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSDeleteDocumentRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSDeleteDocumentRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSBatchGetDocumentsRequest
+
+@implementation GCFSBatchGetDocumentsRequest
+
+@dynamic consistencySelectorOneOfCase;
+@dynamic database;
+@dynamic documentsArray, documentsArray_Count;
+@dynamic hasMask, mask;
+@dynamic transaction;
+@dynamic newTransaction;
+@dynamic readTime;
+
+typedef struct GCFSBatchGetDocumentsRequest__storage_ {
+ uint32_t _has_storage_[2];
+ NSString *database;
+ NSMutableArray *documentsArray;
+ GCFSDocumentMask *mask;
+ NSData *transaction;
+ GCFSTransactionOptions *newTransaction;
+ GPBTimestamp *readTime;
+} GCFSBatchGetDocumentsRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "database",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSBatchGetDocumentsRequest_FieldNumber_Database,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, database),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "documentsArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSBatchGetDocumentsRequest_FieldNumber_DocumentsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, documentsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "mask",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask),
+ .number = GCFSBatchGetDocumentsRequest_FieldNumber_Mask,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, mask),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "transaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSBatchGetDocumentsRequest_FieldNumber_Transaction,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, transaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "newTransaction",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions),
+ .number = GCFSBatchGetDocumentsRequest_FieldNumber_NewTransaction,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, newTransaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSBatchGetDocumentsRequest_FieldNumber_ReadTime,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSBatchGetDocumentsRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSBatchGetDocumentsRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "consistencySelector",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSBatchGetDocumentsRequest_ClearConsistencySelectorOneOfCase(GCFSBatchGetDocumentsRequest *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSBatchGetDocumentsResponse
+
+@implementation GCFSBatchGetDocumentsResponse
+
+@dynamic resultOneOfCase;
+@dynamic found;
+@dynamic missing;
+@dynamic transaction;
+@dynamic hasReadTime, readTime;
+
+typedef struct GCFSBatchGetDocumentsResponse__storage_ {
+ uint32_t _has_storage_[2];
+ GCFSDocument *found;
+ NSString *missing;
+ NSData *transaction;
+ GPBTimestamp *readTime;
+} GCFSBatchGetDocumentsResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "found",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument),
+ .number = GCFSBatchGetDocumentsResponse_FieldNumber_Found,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsResponse__storage_, found),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "missing",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSBatchGetDocumentsResponse_FieldNumber_Missing,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsResponse__storage_, missing),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "transaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSBatchGetDocumentsResponse_FieldNumber_Transaction,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsResponse__storage_, transaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSBatchGetDocumentsResponse_FieldNumber_ReadTime,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsResponse__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSBatchGetDocumentsResponse class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSBatchGetDocumentsResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "result",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSBatchGetDocumentsResponse_ClearResultOneOfCase(GCFSBatchGetDocumentsResponse *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSBeginTransactionRequest
+
+@implementation GCFSBeginTransactionRequest
+
+@dynamic database;
+@dynamic hasOptions, options;
+
+typedef struct GCFSBeginTransactionRequest__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *database;
+ GCFSTransactionOptions *options;
+} GCFSBeginTransactionRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "database",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSBeginTransactionRequest_FieldNumber_Database,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSBeginTransactionRequest__storage_, database),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "options",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions),
+ .number = GCFSBeginTransactionRequest_FieldNumber_Options,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSBeginTransactionRequest__storage_, options),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSBeginTransactionRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSBeginTransactionRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSBeginTransactionResponse
+
+@implementation GCFSBeginTransactionResponse
+
+@dynamic transaction;
+
+typedef struct GCFSBeginTransactionResponse__storage_ {
+ uint32_t _has_storage_[1];
+ NSData *transaction;
+} GCFSBeginTransactionResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "transaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSBeginTransactionResponse_FieldNumber_Transaction,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSBeginTransactionResponse__storage_, transaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSBeginTransactionResponse class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSBeginTransactionResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSCommitRequest
+
+@implementation GCFSCommitRequest
+
+@dynamic database;
+@dynamic writesArray, writesArray_Count;
+@dynamic transaction;
+
+typedef struct GCFSCommitRequest__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *database;
+ NSMutableArray *writesArray;
+ NSData *transaction;
+} GCFSCommitRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "database",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSCommitRequest_FieldNumber_Database,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSCommitRequest__storage_, database),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "writesArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSWrite),
+ .number = GCFSCommitRequest_FieldNumber_WritesArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSCommitRequest__storage_, writesArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "transaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSCommitRequest_FieldNumber_Transaction,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSCommitRequest__storage_, transaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSCommitRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSCommitRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSCommitResponse
+
+@implementation GCFSCommitResponse
+
+@dynamic writeResultsArray, writeResultsArray_Count;
+@dynamic hasCommitTime, commitTime;
+
+typedef struct GCFSCommitResponse__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *writeResultsArray;
+ GPBTimestamp *commitTime;
+} GCFSCommitResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "writeResultsArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSWriteResult),
+ .number = GCFSCommitResponse_FieldNumber_WriteResultsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSCommitResponse__storage_, writeResultsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "commitTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSCommitResponse_FieldNumber_CommitTime,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSCommitResponse__storage_, commitTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSCommitResponse class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSCommitResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSRollbackRequest
+
+@implementation GCFSRollbackRequest
+
+@dynamic database;
+@dynamic transaction;
+
+typedef struct GCFSRollbackRequest__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *database;
+ NSData *transaction;
+} GCFSRollbackRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "database",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSRollbackRequest_FieldNumber_Database,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSRollbackRequest__storage_, database),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "transaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSRollbackRequest_FieldNumber_Transaction,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSRollbackRequest__storage_, transaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSRollbackRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSRollbackRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSRunQueryRequest
+
+@implementation GCFSRunQueryRequest
+
+@dynamic queryTypeOneOfCase;
+@dynamic consistencySelectorOneOfCase;
+@dynamic parent;
+@dynamic structuredQuery;
+@dynamic transaction;
+@dynamic newTransaction;
+@dynamic readTime;
+
+typedef struct GCFSRunQueryRequest__storage_ {
+ uint32_t _has_storage_[3];
+ NSString *parent;
+ GCFSStructuredQuery *structuredQuery;
+ NSData *transaction;
+ GCFSTransactionOptions *newTransaction;
+ GPBTimestamp *readTime;
+} GCFSRunQueryRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "parent",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSRunQueryRequest_FieldNumber_Parent,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, parent),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "structuredQuery",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery),
+ .number = GCFSRunQueryRequest_FieldNumber_StructuredQuery,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, structuredQuery),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "transaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSRunQueryRequest_FieldNumber_Transaction,
+ .hasIndex = -2,
+ .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, transaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "newTransaction",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions),
+ .number = GCFSRunQueryRequest_FieldNumber_NewTransaction,
+ .hasIndex = -2,
+ .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, newTransaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSRunQueryRequest_FieldNumber_ReadTime,
+ .hasIndex = -2,
+ .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSRunQueryRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSRunQueryRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "queryType",
+ "consistencySelector",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSRunQueryRequest_ClearQueryTypeOneOfCase(GCFSRunQueryRequest *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+void GCFSRunQueryRequest_ClearConsistencySelectorOneOfCase(GCFSRunQueryRequest *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:1];
+ GPBMaybeClearOneof(message, oneof, -2, 0);
+}
+#pragma mark - GCFSRunQueryResponse
+
+@implementation GCFSRunQueryResponse
+
+@dynamic transaction;
+@dynamic hasDocument, document;
+@dynamic hasReadTime, readTime;
+@dynamic skippedResults;
+
+typedef struct GCFSRunQueryResponse__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t skippedResults;
+ GCFSDocument *document;
+ NSData *transaction;
+ GPBTimestamp *readTime;
+} GCFSRunQueryResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "document",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument),
+ .number = GCFSRunQueryResponse_FieldNumber_Document,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSRunQueryResponse__storage_, document),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "transaction",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSRunQueryResponse_FieldNumber_Transaction,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSRunQueryResponse__storage_, transaction),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSRunQueryResponse_FieldNumber_ReadTime,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSRunQueryResponse__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "skippedResults",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSRunQueryResponse_FieldNumber_SkippedResults,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GCFSRunQueryResponse__storage_, skippedResults),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSRunQueryResponse class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSRunQueryResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSWriteRequest
+
+@implementation GCFSWriteRequest
+
+@dynamic database;
+@dynamic streamId;
+@dynamic writesArray, writesArray_Count;
+@dynamic streamToken;
+@dynamic labels, labels_Count;
+
+typedef struct GCFSWriteRequest__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *database;
+ NSString *streamId;
+ NSMutableArray *writesArray;
+ NSData *streamToken;
+ NSMutableDictionary *labels;
+} GCFSWriteRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "database",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSWriteRequest_FieldNumber_Database,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, database),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSWriteRequest_FieldNumber_StreamId,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "writesArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSWrite),
+ .number = GCFSWriteRequest_FieldNumber_WritesArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, writesArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "streamToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSWriteRequest_FieldNumber_StreamToken,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, streamToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "labels",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSWriteRequest_FieldNumber_Labels,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, labels),
+ .flags = GPBFieldMapKeyString,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSWriteRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSWriteRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSWriteResponse
+
+@implementation GCFSWriteResponse
+
+@dynamic streamId;
+@dynamic streamToken;
+@dynamic writeResultsArray, writeResultsArray_Count;
+@dynamic hasCommitTime, commitTime;
+
+typedef struct GCFSWriteResponse__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *streamId;
+ NSData *streamToken;
+ NSMutableArray *writeResultsArray;
+ GPBTimestamp *commitTime;
+} GCFSWriteResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "streamId",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSWriteResponse_FieldNumber_StreamId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSWriteResponse__storage_, streamId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "streamToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSWriteResponse_FieldNumber_StreamToken,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSWriteResponse__storage_, streamToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "writeResultsArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSWriteResult),
+ .number = GCFSWriteResponse_FieldNumber_WriteResultsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSWriteResponse__storage_, writeResultsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "commitTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSWriteResponse_FieldNumber_CommitTime,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSWriteResponse__storage_, commitTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSWriteResponse class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSWriteResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSListenRequest
+
+@implementation GCFSListenRequest
+
+@dynamic targetChangeOneOfCase;
+@dynamic database;
+@dynamic addTarget;
+@dynamic removeTarget;
+@dynamic labels, labels_Count;
+
+typedef struct GCFSListenRequest__storage_ {
+ uint32_t _has_storage_[2];
+ int32_t removeTarget;
+ NSString *database;
+ GCFSTarget *addTarget;
+ NSMutableDictionary *labels;
+} GCFSListenRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "database",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListenRequest_FieldNumber_Database,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSListenRequest__storage_, database),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "addTarget",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget),
+ .number = GCFSListenRequest_FieldNumber_AddTarget,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSListenRequest__storage_, addTarget),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "removeTarget",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListenRequest_FieldNumber_RemoveTarget,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSListenRequest__storage_, removeTarget),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "labels",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListenRequest_FieldNumber_Labels,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSListenRequest__storage_, labels),
+ .flags = GPBFieldMapKeyString,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSListenRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSListenRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "targetChange",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSListenRequest_ClearTargetChangeOneOfCase(GCFSListenRequest *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSListenResponse
+
+@implementation GCFSListenResponse
+
+@dynamic responseTypeOneOfCase;
+@dynamic targetChange;
+@dynamic documentChange;
+@dynamic documentDelete;
+@dynamic documentRemove;
+@dynamic filter;
+
+typedef struct GCFSListenResponse__storage_ {
+ uint32_t _has_storage_[2];
+ GCFSTargetChange *targetChange;
+ GCFSDocumentChange *documentChange;
+ GCFSDocumentDelete *documentDelete;
+ GCFSExistenceFilter *filter;
+ GCFSDocumentRemove *documentRemove;
+} GCFSListenResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "targetChange",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTargetChange),
+ .number = GCFSListenResponse_FieldNumber_TargetChange,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, targetChange),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "documentChange",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentChange),
+ .number = GCFSListenResponse_FieldNumber_DocumentChange,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, documentChange),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "documentDelete",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentDelete),
+ .number = GCFSListenResponse_FieldNumber_DocumentDelete,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, documentDelete),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "filter",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSExistenceFilter),
+ .number = GCFSListenResponse_FieldNumber_Filter,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, filter),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "documentRemove",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentRemove),
+ .number = GCFSListenResponse_FieldNumber_DocumentRemove,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, documentRemove),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSListenResponse class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSListenResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "responseType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSListenResponse_ClearResponseTypeOneOfCase(GCFSListenResponse *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSTarget
+
+@implementation GCFSTarget
+
+@dynamic targetTypeOneOfCase;
+@dynamic resumeTypeOneOfCase;
+@dynamic query;
+@dynamic documents;
+@dynamic resumeToken;
+@dynamic readTime;
+@dynamic targetId;
+@dynamic once;
+
+typedef struct GCFSTarget__storage_ {
+ uint32_t _has_storage_[3];
+ int32_t targetId;
+ GCFSTarget_QueryTarget *query;
+ GCFSTarget_DocumentsTarget *documents;
+ NSData *resumeToken;
+ GPBTimestamp *readTime;
+} GCFSTarget__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "query",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget_QueryTarget),
+ .number = GCFSTarget_FieldNumber_Query,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSTarget__storage_, query),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "documents",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget_DocumentsTarget),
+ .number = GCFSTarget_FieldNumber_Documents,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSTarget__storage_, documents),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "resumeToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSTarget_FieldNumber_ResumeToken,
+ .hasIndex = -2,
+ .offset = (uint32_t)offsetof(GCFSTarget__storage_, resumeToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "targetId",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSTarget_FieldNumber_TargetId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSTarget__storage_, targetId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "once",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSTarget_FieldNumber_Once,
+ .hasIndex = 1,
+ .offset = 2, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSTarget_FieldNumber_ReadTime,
+ .hasIndex = -2,
+ .offset = (uint32_t)offsetof(GCFSTarget__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSTarget class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSTarget__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "targetType",
+ "resumeType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSTarget_ClearTargetTypeOneOfCase(GCFSTarget *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+void GCFSTarget_ClearResumeTypeOneOfCase(GCFSTarget *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:1];
+ GPBMaybeClearOneof(message, oneof, -2, 0);
+}
+#pragma mark - GCFSTarget_DocumentsTarget
+
+@implementation GCFSTarget_DocumentsTarget
+
+@dynamic documentsArray, documentsArray_Count;
+
+typedef struct GCFSTarget_DocumentsTarget__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *documentsArray;
+} GCFSTarget_DocumentsTarget__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "documentsArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSTarget_DocumentsTarget_FieldNumber_DocumentsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSTarget_DocumentsTarget__storage_, documentsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSTarget_DocumentsTarget class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSTarget_DocumentsTarget__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSTarget)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSTarget_QueryTarget
+
+@implementation GCFSTarget_QueryTarget
+
+@dynamic queryTypeOneOfCase;
+@dynamic parent;
+@dynamic structuredQuery;
+
+typedef struct GCFSTarget_QueryTarget__storage_ {
+ uint32_t _has_storage_[2];
+ NSString *parent;
+ GCFSStructuredQuery *structuredQuery;
+} GCFSTarget_QueryTarget__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "parent",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSTarget_QueryTarget_FieldNumber_Parent,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSTarget_QueryTarget__storage_, parent),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "structuredQuery",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery),
+ .number = GCFSTarget_QueryTarget_FieldNumber_StructuredQuery,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSTarget_QueryTarget__storage_, structuredQuery),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSTarget_QueryTarget class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSTarget_QueryTarget__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "queryType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSTarget)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSTarget_QueryTarget_ClearQueryTypeOneOfCase(GCFSTarget_QueryTarget *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSTargetChange
+
+@implementation GCFSTargetChange
+
+@dynamic targetChangeType;
+@dynamic targetIdsArray, targetIdsArray_Count;
+@dynamic hasCause, cause;
+@dynamic resumeToken;
+@dynamic hasReadTime, readTime;
+
+typedef struct GCFSTargetChange__storage_ {
+ uint32_t _has_storage_[1];
+ GCFSTargetChange_TargetChangeType targetChangeType;
+ GPBInt32Array *targetIdsArray;
+ RPCStatus *cause;
+ NSData *resumeToken;
+ GPBTimestamp *readTime;
+} GCFSTargetChange__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "targetChangeType",
+ .dataTypeSpecific.enumDescFunc = GCFSTargetChange_TargetChangeType_EnumDescriptor,
+ .number = GCFSTargetChange_FieldNumber_TargetChangeType,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, targetChangeType),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "targetIdsArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSTargetChange_FieldNumber_TargetIdsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, targetIdsArray),
+ .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked),
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "cause",
+ .dataTypeSpecific.className = GPBStringifySymbol(RPCStatus),
+ .number = GCFSTargetChange_FieldNumber_Cause,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, cause),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "resumeToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSTargetChange_FieldNumber_ResumeToken,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, resumeToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBytes,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSTargetChange_FieldNumber_ReadTime,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSTargetChange class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSTargetChange__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+int32_t GCFSTargetChange_TargetChangeType_RawValue(GCFSTargetChange *message) {
+ GPBDescriptor *descriptor = [GCFSTargetChange descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSTargetChange_FieldNumber_TargetChangeType];
+ return GPBGetMessageInt32Field(message, field);
+}
+
+void SetGCFSTargetChange_TargetChangeType_RawValue(GCFSTargetChange *message, int32_t value) {
+ GPBDescriptor *descriptor = [GCFSTargetChange descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSTargetChange_FieldNumber_TargetChangeType];
+ GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax);
+}
+
+#pragma mark - Enum GCFSTargetChange_TargetChangeType
+
+GPBEnumDescriptor *GCFSTargetChange_TargetChangeType_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "NoChange\000Add\000Remove\000Current\000Reset\000";
+ static const int32_t values[] = {
+ GCFSTargetChange_TargetChangeType_NoChange,
+ GCFSTargetChange_TargetChangeType_Add,
+ GCFSTargetChange_TargetChangeType_Remove,
+ GCFSTargetChange_TargetChangeType_Current,
+ GCFSTargetChange_TargetChangeType_Reset,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSTargetChange_TargetChangeType)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GCFSTargetChange_TargetChangeType_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GCFSTargetChange_TargetChangeType_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GCFSTargetChange_TargetChangeType_NoChange:
+ case GCFSTargetChange_TargetChangeType_Add:
+ case GCFSTargetChange_TargetChangeType_Remove:
+ case GCFSTargetChange_TargetChangeType_Current:
+ case GCFSTargetChange_TargetChangeType_Reset:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GCFSListCollectionIdsRequest
+
+@implementation GCFSListCollectionIdsRequest
+
+@dynamic parent;
+@dynamic pageSize;
+@dynamic pageToken;
+
+typedef struct GCFSListCollectionIdsRequest__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t pageSize;
+ NSString *parent;
+ NSString *pageToken;
+} GCFSListCollectionIdsRequest__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "parent",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListCollectionIdsRequest_FieldNumber_Parent,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSListCollectionIdsRequest__storage_, parent),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "pageSize",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListCollectionIdsRequest_FieldNumber_PageSize,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSListCollectionIdsRequest__storage_, pageSize),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "pageToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListCollectionIdsRequest_FieldNumber_PageToken,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSListCollectionIdsRequest__storage_, pageToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSListCollectionIdsRequest class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSListCollectionIdsRequest__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSListCollectionIdsResponse
+
+@implementation GCFSListCollectionIdsResponse
+
+@dynamic collectionIdsArray, collectionIdsArray_Count;
+@dynamic nextPageToken;
+
+typedef struct GCFSListCollectionIdsResponse__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *collectionIdsArray;
+ NSString *nextPageToken;
+} GCFSListCollectionIdsResponse__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "collectionIdsArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListCollectionIdsResponse_FieldNumber_CollectionIdsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSListCollectionIdsResponse__storage_, collectionIdsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "nextPageToken",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSListCollectionIdsResponse_FieldNumber_NextPageToken,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSListCollectionIdsResponse__storage_, nextPageToken),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSListCollectionIdsResponse class]
+ rootClass:[GCFSFirestoreRoot class]
+ file:GCFSFirestoreRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSListCollectionIdsResponse__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h
new file mode 100644
index 0000000..5704c2b
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Firestore.pbobjc.h"
+
+#import <ProtoRPC/ProtoService.h>
+#import <ProtoRPC/ProtoRPC.h>
+#import <RxLibrary/GRXWriteable.h>
+#import <RxLibrary/GRXWriter.h>
+
+#import "Annotations.pbobjc.h"
+#import "Common.pbobjc.h"
+#import "Document.pbobjc.h"
+#import "Query.pbobjc.h"
+#import "Write.pbobjc.h"
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Empty.pbobjc.h>
+#else
+ #import "Empty.pbobjc.h"
+#endif
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Timestamp.pbobjc.h>
+#else
+ #import "Timestamp.pbobjc.h"
+#endif
+#import "Status.pbobjc.h"
+
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol GCFSFirestore <NSObject>
+
+#pragma mark GetDocument(GetDocumentRequest) returns (Document)
+
+/**
+ * Gets a single document.
+ */
+- (void)getDocumentWithRequest:(GCFSGetDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler;
+
+/**
+ * Gets a single document.
+ */
+- (GRPCProtoCall *)RPCToGetDocumentWithRequest:(GCFSGetDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler;
+
+
+#pragma mark ListDocuments(ListDocumentsRequest) returns (ListDocumentsResponse)
+
+/**
+ * Lists documents.
+ */
+- (void)listDocumentsWithRequest:(GCFSListDocumentsRequest *)request handler:(void(^)(GCFSListDocumentsResponse *_Nullable response, NSError *_Nullable error))handler;
+
+/**
+ * Lists documents.
+ */
+- (GRPCProtoCall *)RPCToListDocumentsWithRequest:(GCFSListDocumentsRequest *)request handler:(void(^)(GCFSListDocumentsResponse *_Nullable response, NSError *_Nullable error))handler;
+
+
+#pragma mark CreateDocument(CreateDocumentRequest) returns (Document)
+
+/**
+ * Creates a new document.
+ */
+- (void)createDocumentWithRequest:(GCFSCreateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler;
+
+/**
+ * Creates a new document.
+ */
+- (GRPCProtoCall *)RPCToCreateDocumentWithRequest:(GCFSCreateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler;
+
+
+#pragma mark UpdateDocument(UpdateDocumentRequest) returns (Document)
+
+/**
+ * Updates or inserts a document.
+ */
+- (void)updateDocumentWithRequest:(GCFSUpdateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler;
+
+/**
+ * Updates or inserts a document.
+ */
+- (GRPCProtoCall *)RPCToUpdateDocumentWithRequest:(GCFSUpdateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler;
+
+
+#pragma mark DeleteDocument(DeleteDocumentRequest) returns (Empty)
+
+/**
+ * Deletes a document.
+ */
+- (void)deleteDocumentWithRequest:(GCFSDeleteDocumentRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler;
+
+/**
+ * Deletes a document.
+ */
+- (GRPCProtoCall *)RPCToDeleteDocumentWithRequest:(GCFSDeleteDocumentRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler;
+
+
+#pragma mark BatchGetDocuments(BatchGetDocumentsRequest) returns (stream BatchGetDocumentsResponse)
+
+/**
+ * Gets multiple documents.
+ *
+ * Documents returned by this method are not guaranteed to be returned in the
+ * same order that they were requested.
+ */
+- (void)batchGetDocumentsWithRequest:(GCFSBatchGetDocumentsRequest *)request eventHandler:(void(^)(BOOL done, GCFSBatchGetDocumentsResponse *_Nullable response, NSError *_Nullable error))eventHandler;
+
+/**
+ * Gets multiple documents.
+ *
+ * Documents returned by this method are not guaranteed to be returned in the
+ * same order that they were requested.
+ */
+- (GRPCProtoCall *)RPCToBatchGetDocumentsWithRequest:(GCFSBatchGetDocumentsRequest *)request eventHandler:(void(^)(BOOL done, GCFSBatchGetDocumentsResponse *_Nullable response, NSError *_Nullable error))eventHandler;
+
+
+#pragma mark BeginTransaction(BeginTransactionRequest) returns (BeginTransactionResponse)
+
+/**
+ * Starts a new transaction.
+ */
+- (void)beginTransactionWithRequest:(GCFSBeginTransactionRequest *)request handler:(void(^)(GCFSBeginTransactionResponse *_Nullable response, NSError *_Nullable error))handler;
+
+/**
+ * Starts a new transaction.
+ */
+- (GRPCProtoCall *)RPCToBeginTransactionWithRequest:(GCFSBeginTransactionRequest *)request handler:(void(^)(GCFSBeginTransactionResponse *_Nullable response, NSError *_Nullable error))handler;
+
+
+#pragma mark Commit(CommitRequest) returns (CommitResponse)
+
+/**
+ * Commits a transaction, while optionally updating documents.
+ */
+- (void)commitWithRequest:(GCFSCommitRequest *)request handler:(void(^)(GCFSCommitResponse *_Nullable response, NSError *_Nullable error))handler;
+
+/**
+ * Commits a transaction, while optionally updating documents.
+ */
+- (GRPCProtoCall *)RPCToCommitWithRequest:(GCFSCommitRequest *)request handler:(void(^)(GCFSCommitResponse *_Nullable response, NSError *_Nullable error))handler;
+
+
+#pragma mark Rollback(RollbackRequest) returns (Empty)
+
+/**
+ * Rolls back a transaction.
+ */
+- (void)rollbackWithRequest:(GCFSRollbackRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler;
+
+/**
+ * Rolls back a transaction.
+ */
+- (GRPCProtoCall *)RPCToRollbackWithRequest:(GCFSRollbackRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler;
+
+
+#pragma mark RunQuery(RunQueryRequest) returns (stream RunQueryResponse)
+
+/**
+ * Runs a query.
+ */
+- (void)runQueryWithRequest:(GCFSRunQueryRequest *)request eventHandler:(void(^)(BOOL done, GCFSRunQueryResponse *_Nullable response, NSError *_Nullable error))eventHandler;
+
+/**
+ * Runs a query.
+ */
+- (GRPCProtoCall *)RPCToRunQueryWithRequest:(GCFSRunQueryRequest *)request eventHandler:(void(^)(BOOL done, GCFSRunQueryResponse *_Nullable response, NSError *_Nullable error))eventHandler;
+
+
+#pragma mark Write(stream WriteRequest) returns (stream WriteResponse)
+
+/**
+ * Streams batches of document updates and deletes, in order.
+ */
+- (void)writeWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSWriteResponse *_Nullable response, NSError *_Nullable error))eventHandler;
+
+/**
+ * Streams batches of document updates and deletes, in order.
+ */
+- (GRPCProtoCall *)RPCToWriteWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSWriteResponse *_Nullable response, NSError *_Nullable error))eventHandler;
+
+
+#pragma mark Listen(stream ListenRequest) returns (stream ListenResponse)
+
+/**
+ * Listens to changes.
+ */
+- (void)listenWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSListenResponse *_Nullable response, NSError *_Nullable error))eventHandler;
+
+/**
+ * Listens to changes.
+ */
+- (GRPCProtoCall *)RPCToListenWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSListenResponse *_Nullable response, NSError *_Nullable error))eventHandler;
+
+
+#pragma mark ListCollectionIds(ListCollectionIdsRequest) returns (ListCollectionIdsResponse)
+
+/**
+ * Lists all the collection IDs underneath a document.
+ */
+- (void)listCollectionIdsWithRequest:(GCFSListCollectionIdsRequest *)request handler:(void(^)(GCFSListCollectionIdsResponse *_Nullable response, NSError *_Nullable error))handler;
+
+/**
+ * Lists all the collection IDs underneath a document.
+ */
+- (GRPCProtoCall *)RPCToListCollectionIdsWithRequest:(GCFSListCollectionIdsRequest *)request handler:(void(^)(GCFSListCollectionIdsResponse *_Nullable response, NSError *_Nullable error))handler;
+
+
+@end
+
+/**
+ * Basic service implementation, over gRPC, that only does
+ * marshalling and parsing.
+ */
+@interface GCFSFirestore : GRPCProtoService<GCFSFirestore>
+- (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER;
++ (instancetype)serviceWithHost:(NSString *)host;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.m
new file mode 100644
index 0000000..a3e338d
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.m
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Firestore.pbrpc.h"
+
+#import <ProtoRPC/ProtoRPC.h>
+#import <RxLibrary/GRXWriter+Immediate.h>
+
+@implementation GCFSFirestore
+
+// Designated initializer
+- (instancetype)initWithHost:(NSString *)host {
+ return (self = [super initWithHost:host packageName:@"google.firestore.v1beta1" serviceName:@"Firestore"]);
+}
+
+// Override superclass initializer to disallow different package and service names.
+- (instancetype)initWithHost:(NSString *)host
+ packageName:(NSString *)packageName
+ serviceName:(NSString *)serviceName {
+ return [self initWithHost:host];
+}
+
++ (instancetype)serviceWithHost:(NSString *)host {
+ return [[self alloc] initWithHost:host];
+}
+
+
+#pragma mark GetDocument(GetDocumentRequest) returns (Document)
+
+/**
+ * Gets a single document.
+ */
+- (void)getDocumentWithRequest:(GCFSGetDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{
+ [[self RPCToGetDocumentWithRequest:request handler:handler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Gets a single document.
+ */
+- (GRPCProtoCall *)RPCToGetDocumentWithRequest:(GCFSGetDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{
+ return [self RPCToMethod:@"GetDocument"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GCFSDocument class]
+ responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]];
+}
+#pragma mark ListDocuments(ListDocumentsRequest) returns (ListDocumentsResponse)
+
+/**
+ * Lists documents.
+ */
+- (void)listDocumentsWithRequest:(GCFSListDocumentsRequest *)request handler:(void(^)(GCFSListDocumentsResponse *_Nullable response, NSError *_Nullable error))handler{
+ [[self RPCToListDocumentsWithRequest:request handler:handler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Lists documents.
+ */
+- (GRPCProtoCall *)RPCToListDocumentsWithRequest:(GCFSListDocumentsRequest *)request handler:(void(^)(GCFSListDocumentsResponse *_Nullable response, NSError *_Nullable error))handler{
+ return [self RPCToMethod:@"ListDocuments"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GCFSListDocumentsResponse class]
+ responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]];
+}
+#pragma mark CreateDocument(CreateDocumentRequest) returns (Document)
+
+/**
+ * Creates a new document.
+ */
+- (void)createDocumentWithRequest:(GCFSCreateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{
+ [[self RPCToCreateDocumentWithRequest:request handler:handler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Creates a new document.
+ */
+- (GRPCProtoCall *)RPCToCreateDocumentWithRequest:(GCFSCreateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{
+ return [self RPCToMethod:@"CreateDocument"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GCFSDocument class]
+ responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]];
+}
+#pragma mark UpdateDocument(UpdateDocumentRequest) returns (Document)
+
+/**
+ * Updates or inserts a document.
+ */
+- (void)updateDocumentWithRequest:(GCFSUpdateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{
+ [[self RPCToUpdateDocumentWithRequest:request handler:handler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Updates or inserts a document.
+ */
+- (GRPCProtoCall *)RPCToUpdateDocumentWithRequest:(GCFSUpdateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{
+ return [self RPCToMethod:@"UpdateDocument"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GCFSDocument class]
+ responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]];
+}
+#pragma mark DeleteDocument(DeleteDocumentRequest) returns (Empty)
+
+/**
+ * Deletes a document.
+ */
+- (void)deleteDocumentWithRequest:(GCFSDeleteDocumentRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler{
+ [[self RPCToDeleteDocumentWithRequest:request handler:handler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Deletes a document.
+ */
+- (GRPCProtoCall *)RPCToDeleteDocumentWithRequest:(GCFSDeleteDocumentRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler{
+ return [self RPCToMethod:@"DeleteDocument"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GPBEmpty class]
+ responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]];
+}
+#pragma mark BatchGetDocuments(BatchGetDocumentsRequest) returns (stream BatchGetDocumentsResponse)
+
+/**
+ * Gets multiple documents.
+ *
+ * Documents returned by this method are not guaranteed to be returned in the
+ * same order that they were requested.
+ */
+- (void)batchGetDocumentsWithRequest:(GCFSBatchGetDocumentsRequest *)request eventHandler:(void(^)(BOOL done, GCFSBatchGetDocumentsResponse *_Nullable response, NSError *_Nullable error))eventHandler{
+ [[self RPCToBatchGetDocumentsWithRequest:request eventHandler:eventHandler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Gets multiple documents.
+ *
+ * Documents returned by this method are not guaranteed to be returned in the
+ * same order that they were requested.
+ */
+- (GRPCProtoCall *)RPCToBatchGetDocumentsWithRequest:(GCFSBatchGetDocumentsRequest *)request eventHandler:(void(^)(BOOL done, GCFSBatchGetDocumentsResponse *_Nullable response, NSError *_Nullable error))eventHandler{
+ return [self RPCToMethod:@"BatchGetDocuments"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GCFSBatchGetDocumentsResponse class]
+ responsesWriteable:[GRXWriteable writeableWithEventHandler:eventHandler]];
+}
+#pragma mark BeginTransaction(BeginTransactionRequest) returns (BeginTransactionResponse)
+
+/**
+ * Starts a new transaction.
+ */
+- (void)beginTransactionWithRequest:(GCFSBeginTransactionRequest *)request handler:(void(^)(GCFSBeginTransactionResponse *_Nullable response, NSError *_Nullable error))handler{
+ [[self RPCToBeginTransactionWithRequest:request handler:handler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Starts a new transaction.
+ */
+- (GRPCProtoCall *)RPCToBeginTransactionWithRequest:(GCFSBeginTransactionRequest *)request handler:(void(^)(GCFSBeginTransactionResponse *_Nullable response, NSError *_Nullable error))handler{
+ return [self RPCToMethod:@"BeginTransaction"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GCFSBeginTransactionResponse class]
+ responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]];
+}
+#pragma mark Commit(CommitRequest) returns (CommitResponse)
+
+/**
+ * Commits a transaction, while optionally updating documents.
+ */
+- (void)commitWithRequest:(GCFSCommitRequest *)request handler:(void(^)(GCFSCommitResponse *_Nullable response, NSError *_Nullable error))handler{
+ [[self RPCToCommitWithRequest:request handler:handler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Commits a transaction, while optionally updating documents.
+ */
+- (GRPCProtoCall *)RPCToCommitWithRequest:(GCFSCommitRequest *)request handler:(void(^)(GCFSCommitResponse *_Nullable response, NSError *_Nullable error))handler{
+ return [self RPCToMethod:@"Commit"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GCFSCommitResponse class]
+ responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]];
+}
+#pragma mark Rollback(RollbackRequest) returns (Empty)
+
+/**
+ * Rolls back a transaction.
+ */
+- (void)rollbackWithRequest:(GCFSRollbackRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler{
+ [[self RPCToRollbackWithRequest:request handler:handler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Rolls back a transaction.
+ */
+- (GRPCProtoCall *)RPCToRollbackWithRequest:(GCFSRollbackRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler{
+ return [self RPCToMethod:@"Rollback"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GPBEmpty class]
+ responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]];
+}
+#pragma mark RunQuery(RunQueryRequest) returns (stream RunQueryResponse)
+
+/**
+ * Runs a query.
+ */
+- (void)runQueryWithRequest:(GCFSRunQueryRequest *)request eventHandler:(void(^)(BOOL done, GCFSRunQueryResponse *_Nullable response, NSError *_Nullable error))eventHandler{
+ [[self RPCToRunQueryWithRequest:request eventHandler:eventHandler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Runs a query.
+ */
+- (GRPCProtoCall *)RPCToRunQueryWithRequest:(GCFSRunQueryRequest *)request eventHandler:(void(^)(BOOL done, GCFSRunQueryResponse *_Nullable response, NSError *_Nullable error))eventHandler{
+ return [self RPCToMethod:@"RunQuery"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GCFSRunQueryResponse class]
+ responsesWriteable:[GRXWriteable writeableWithEventHandler:eventHandler]];
+}
+#pragma mark Write(stream WriteRequest) returns (stream WriteResponse)
+
+/**
+ * Streams batches of document updates and deletes, in order.
+ */
+- (void)writeWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSWriteResponse *_Nullable response, NSError *_Nullable error))eventHandler{
+ [[self RPCToWriteWithRequestsWriter:requestWriter eventHandler:eventHandler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Streams batches of document updates and deletes, in order.
+ */
+- (GRPCProtoCall *)RPCToWriteWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSWriteResponse *_Nullable response, NSError *_Nullable error))eventHandler{
+ return [self RPCToMethod:@"Write"
+ requestsWriter:requestWriter
+ responseClass:[GCFSWriteResponse class]
+ responsesWriteable:[GRXWriteable writeableWithEventHandler:eventHandler]];
+}
+#pragma mark Listen(stream ListenRequest) returns (stream ListenResponse)
+
+/**
+ * Listens to changes.
+ */
+- (void)listenWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSListenResponse *_Nullable response, NSError *_Nullable error))eventHandler{
+ [[self RPCToListenWithRequestsWriter:requestWriter eventHandler:eventHandler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Listens to changes.
+ */
+- (GRPCProtoCall *)RPCToListenWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSListenResponse *_Nullable response, NSError *_Nullable error))eventHandler{
+ return [self RPCToMethod:@"Listen"
+ requestsWriter:requestWriter
+ responseClass:[GCFSListenResponse class]
+ responsesWriteable:[GRXWriteable writeableWithEventHandler:eventHandler]];
+}
+#pragma mark ListCollectionIds(ListCollectionIdsRequest) returns (ListCollectionIdsResponse)
+
+/**
+ * Lists all the collection IDs underneath a document.
+ */
+- (void)listCollectionIdsWithRequest:(GCFSListCollectionIdsRequest *)request handler:(void(^)(GCFSListCollectionIdsResponse *_Nullable response, NSError *_Nullable error))handler{
+ [[self RPCToListCollectionIdsWithRequest:request handler:handler] start];
+}
+// Returns a not-yet-started RPC object.
+/**
+ * Lists all the collection IDs underneath a document.
+ */
+- (GRPCProtoCall *)RPCToListCollectionIdsWithRequest:(GCFSListCollectionIdsRequest *)request handler:(void(^)(GCFSListCollectionIdsResponse *_Nullable response, NSError *_Nullable error))handler{
+ return [self RPCToMethod:@"ListCollectionIds"
+ requestsWriter:[GRXWriter writerWithValue:request]
+ responseClass:[GCFSListCollectionIdsResponse class]
+ responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]];
+}
+@end
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h
new file mode 100644
index 0000000..c2d80e7
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h
@@ -0,0 +1,579 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/query.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GCFSCursor;
+@class GCFSStructuredQuery_CollectionSelector;
+@class GCFSStructuredQuery_CompositeFilter;
+@class GCFSStructuredQuery_FieldFilter;
+@class GCFSStructuredQuery_FieldReference;
+@class GCFSStructuredQuery_Filter;
+@class GCFSStructuredQuery_Order;
+@class GCFSStructuredQuery_Projection;
+@class GCFSStructuredQuery_UnaryFilter;
+@class GCFSValue;
+@class GPBInt32Value;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - Enum GCFSStructuredQuery_Direction
+
+/** A sort direction. */
+typedef GPB_ENUM(GCFSStructuredQuery_Direction) {
+ /**
+ * Value used if any message's field encounters a value that is not defined
+ * by this enum. The message will also have C functions to get/set the rawValue
+ * of the field.
+ **/
+ GCFSStructuredQuery_Direction_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue,
+ /** Unspecified. */
+ GCFSStructuredQuery_Direction_DirectionUnspecified = 0,
+
+ /** Ascending. */
+ GCFSStructuredQuery_Direction_Ascending = 1,
+
+ /** Descending. */
+ GCFSStructuredQuery_Direction_Descending = 2,
+};
+
+GPBEnumDescriptor *GCFSStructuredQuery_Direction_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GCFSStructuredQuery_Direction_IsValidValue(int32_t value);
+
+#pragma mark - Enum GCFSStructuredQuery_CompositeFilter_Operator
+
+/** A composite filter operator. */
+typedef GPB_ENUM(GCFSStructuredQuery_CompositeFilter_Operator) {
+ /**
+ * Value used if any message's field encounters a value that is not defined
+ * by this enum. The message will also have C functions to get/set the rawValue
+ * of the field.
+ **/
+ GCFSStructuredQuery_CompositeFilter_Operator_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue,
+ /** Unspecified. This value must not be used. */
+ GCFSStructuredQuery_CompositeFilter_Operator_OperatorUnspecified = 0,
+
+ /** The results are required to satisfy each of the combined filters. */
+ GCFSStructuredQuery_CompositeFilter_Operator_And = 1,
+};
+
+GPBEnumDescriptor *GCFSStructuredQuery_CompositeFilter_Operator_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GCFSStructuredQuery_CompositeFilter_Operator_IsValidValue(int32_t value);
+
+#pragma mark - Enum GCFSStructuredQuery_FieldFilter_Operator
+
+/** A field filter operator. */
+typedef GPB_ENUM(GCFSStructuredQuery_FieldFilter_Operator) {
+ /**
+ * Value used if any message's field encounters a value that is not defined
+ * by this enum. The message will also have C functions to get/set the rawValue
+ * of the field.
+ **/
+ GCFSStructuredQuery_FieldFilter_Operator_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue,
+ /** Unspecified. This value must not be used. */
+ GCFSStructuredQuery_FieldFilter_Operator_OperatorUnspecified = 0,
+
+ /** Less than. Requires that the field come first in `order_by`. */
+ GCFSStructuredQuery_FieldFilter_Operator_LessThan = 1,
+
+ /** Less than or equal. Requires that the field come first in `order_by`. */
+ GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual = 2,
+
+ /** Greater than. Requires that the field come first in `order_by`. */
+ GCFSStructuredQuery_FieldFilter_Operator_GreaterThan = 3,
+
+ /**
+ * Greater than or equal. Requires that the field come first in
+ * `order_by`.
+ **/
+ GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual = 4,
+
+ /** Equal. */
+ GCFSStructuredQuery_FieldFilter_Operator_Equal = 5,
+};
+
+GPBEnumDescriptor *GCFSStructuredQuery_FieldFilter_Operator_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GCFSStructuredQuery_FieldFilter_Operator_IsValidValue(int32_t value);
+
+#pragma mark - Enum GCFSStructuredQuery_UnaryFilter_Operator
+
+/** A unary operator. */
+typedef GPB_ENUM(GCFSStructuredQuery_UnaryFilter_Operator) {
+ /**
+ * Value used if any message's field encounters a value that is not defined
+ * by this enum. The message will also have C functions to get/set the rawValue
+ * of the field.
+ **/
+ GCFSStructuredQuery_UnaryFilter_Operator_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue,
+ /** Unspecified. This value must not be used. */
+ GCFSStructuredQuery_UnaryFilter_Operator_OperatorUnspecified = 0,
+
+ /** Test if a field is equal to NaN. */
+ GCFSStructuredQuery_UnaryFilter_Operator_IsNan = 2,
+
+ /** Test if an exprestion evaluates to Null. */
+ GCFSStructuredQuery_UnaryFilter_Operator_IsNull = 3,
+};
+
+GPBEnumDescriptor *GCFSStructuredQuery_UnaryFilter_Operator_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GCFSStructuredQuery_UnaryFilter_Operator_IsValidValue(int32_t value);
+
+#pragma mark - GCFSQueryRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GCFSQueryRoot : GPBRootObject
+@end
+
+#pragma mark - GCFSStructuredQuery
+
+typedef GPB_ENUM(GCFSStructuredQuery_FieldNumber) {
+ GCFSStructuredQuery_FieldNumber_Select = 1,
+ GCFSStructuredQuery_FieldNumber_FromArray = 2,
+ GCFSStructuredQuery_FieldNumber_Where = 3,
+ GCFSStructuredQuery_FieldNumber_OrderByArray = 4,
+ GCFSStructuredQuery_FieldNumber_Limit = 5,
+ GCFSStructuredQuery_FieldNumber_Offset = 6,
+ GCFSStructuredQuery_FieldNumber_StartAt = 7,
+ GCFSStructuredQuery_FieldNumber_EndAt = 8,
+};
+
+/**
+ * A Firestore query.
+ **/
+@interface GCFSStructuredQuery : GPBMessage
+
+/** The projection to return. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_Projection *select;
+/** Test to see if @c select has been set. */
+@property(nonatomic, readwrite) BOOL hasSelect;
+
+/** The collections to query. */
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSStructuredQuery_CollectionSelector*> *fromArray;
+/** The number of items in @c fromArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger fromArray_Count;
+
+/** The filter to apply. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_Filter *where;
+/** Test to see if @c where has been set. */
+@property(nonatomic, readwrite) BOOL hasWhere;
+
+/**
+ * The order to apply to the query results.
+ *
+ * Firestore guarantees a stable ordering through the following rules:
+ *
+ * * Any field required to appear in `order_by`, that is not already
+ * specified in `order_by`, is appended to the order in field name order
+ * by default.
+ * * If an order on `__name__` is not specified, it is appended by default.
+ *
+ * Fields are appended with the same sort direction as the last order
+ * specified, or 'ASCENDING' if no order was specified. For example:
+ *
+ * * `SELECT * FROM Foo ORDER BY A` becomes
+ * `SELECT * FROM Foo ORDER BY A, __name__`
+ * * `SELECT * FROM Foo ORDER BY A DESC` becomes
+ * `SELECT * FROM Foo ORDER BY A DESC, __name__ DESC`
+ * * `SELECT * FROM Foo WHERE A > 1` becomes
+ * `SELECT * FROM Foo WHERE A > 1 ORDER BY A, __name__`
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSStructuredQuery_Order*> *orderByArray;
+/** The number of items in @c orderByArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger orderByArray_Count;
+
+/** A starting point for the query results. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSCursor *startAt;
+/** Test to see if @c startAt has been set. */
+@property(nonatomic, readwrite) BOOL hasStartAt;
+
+/** A end point for the query results. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSCursor *endAt;
+/** Test to see if @c endAt has been set. */
+@property(nonatomic, readwrite) BOOL hasEndAt;
+
+/**
+ * The number of results to skip.
+ *
+ * Applies before limit, but after all other constraints. Must be >= 0 if
+ * specified.
+ **/
+@property(nonatomic, readwrite) int32_t offset;
+
+/**
+ * The maximum number of results to return.
+ *
+ * Applies after all other constraints.
+ * Must be >= 0 if specified.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Value *limit;
+/** Test to see if @c limit has been set. */
+@property(nonatomic, readwrite) BOOL hasLimit;
+
+@end
+
+#pragma mark - GCFSStructuredQuery_CollectionSelector
+
+typedef GPB_ENUM(GCFSStructuredQuery_CollectionSelector_FieldNumber) {
+ GCFSStructuredQuery_CollectionSelector_FieldNumber_CollectionId = 2,
+ GCFSStructuredQuery_CollectionSelector_FieldNumber_AllDescendants = 3,
+};
+
+/**
+ * A selection of a collection, such as `messages as m1`.
+ **/
+@interface GCFSStructuredQuery_CollectionSelector : GPBMessage
+
+/**
+ * The collection ID.
+ * When set, selects only collections with this ID.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *collectionId;
+
+/**
+ * When false, selects only collections that are immediate children of
+ * the `parent` specified in the containing `RunQueryRequest`.
+ * When true, selects all descendant collections.
+ **/
+@property(nonatomic, readwrite) BOOL allDescendants;
+
+@end
+
+#pragma mark - GCFSStructuredQuery_Filter
+
+typedef GPB_ENUM(GCFSStructuredQuery_Filter_FieldNumber) {
+ GCFSStructuredQuery_Filter_FieldNumber_CompositeFilter = 1,
+ GCFSStructuredQuery_Filter_FieldNumber_FieldFilter = 2,
+ GCFSStructuredQuery_Filter_FieldNumber_UnaryFilter = 3,
+};
+
+typedef GPB_ENUM(GCFSStructuredQuery_Filter_FilterType_OneOfCase) {
+ GCFSStructuredQuery_Filter_FilterType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter = 1,
+ GCFSStructuredQuery_Filter_FilterType_OneOfCase_FieldFilter = 2,
+ GCFSStructuredQuery_Filter_FilterType_OneOfCase_UnaryFilter = 3,
+};
+
+/**
+ * A filter.
+ **/
+@interface GCFSStructuredQuery_Filter : GPBMessage
+
+/** The type of filter. */
+@property(nonatomic, readonly) GCFSStructuredQuery_Filter_FilterType_OneOfCase filterTypeOneOfCase;
+
+/** A composite filter. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_CompositeFilter *compositeFilter;
+
+/** A filter on a document field. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_FieldFilter *fieldFilter;
+
+/** A filter that takes exactly one argument. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_UnaryFilter *unaryFilter;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'filterType'.
+ **/
+void GCFSStructuredQuery_Filter_ClearFilterTypeOneOfCase(GCFSStructuredQuery_Filter *message);
+
+#pragma mark - GCFSStructuredQuery_CompositeFilter
+
+typedef GPB_ENUM(GCFSStructuredQuery_CompositeFilter_FieldNumber) {
+ GCFSStructuredQuery_CompositeFilter_FieldNumber_Op = 1,
+ GCFSStructuredQuery_CompositeFilter_FieldNumber_FiltersArray = 2,
+};
+
+/**
+ * A filter that merges multiple other filters using the given operator.
+ **/
+@interface GCFSStructuredQuery_CompositeFilter : GPBMessage
+
+/** The operator for combining multiple filters. */
+@property(nonatomic, readwrite) GCFSStructuredQuery_CompositeFilter_Operator op;
+
+/**
+ * The list of filters to combine.
+ * Must contain at least one filter.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSStructuredQuery_Filter*> *filtersArray;
+/** The number of items in @c filtersArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger filtersArray_Count;
+
+@end
+
+/**
+ * Fetches the raw value of a @c GCFSStructuredQuery_CompositeFilter's @c op property, even
+ * if the value was not defined by the enum at the time the code was generated.
+ **/
+int32_t GCFSStructuredQuery_CompositeFilter_Op_RawValue(GCFSStructuredQuery_CompositeFilter *message);
+/**
+ * Sets the raw value of an @c GCFSStructuredQuery_CompositeFilter's @c op property, allowing
+ * it to be set to a value that was not defined by the enum at the time the code
+ * was generated.
+ **/
+void SetGCFSStructuredQuery_CompositeFilter_Op_RawValue(GCFSStructuredQuery_CompositeFilter *message, int32_t value);
+
+#pragma mark - GCFSStructuredQuery_FieldFilter
+
+typedef GPB_ENUM(GCFSStructuredQuery_FieldFilter_FieldNumber) {
+ GCFSStructuredQuery_FieldFilter_FieldNumber_Field = 1,
+ GCFSStructuredQuery_FieldFilter_FieldNumber_Op = 2,
+ GCFSStructuredQuery_FieldFilter_FieldNumber_Value = 3,
+};
+
+/**
+ * A filter on a specific field.
+ **/
+@interface GCFSStructuredQuery_FieldFilter : GPBMessage
+
+/** The field to filter by. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_FieldReference *field;
+/** Test to see if @c field has been set. */
+@property(nonatomic, readwrite) BOOL hasField;
+
+/** The operator to filter by. */
+@property(nonatomic, readwrite) GCFSStructuredQuery_FieldFilter_Operator op;
+
+/** The value to compare to. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSValue *value;
+/** Test to see if @c value has been set. */
+@property(nonatomic, readwrite) BOOL hasValue;
+
+@end
+
+/**
+ * Fetches the raw value of a @c GCFSStructuredQuery_FieldFilter's @c op property, even
+ * if the value was not defined by the enum at the time the code was generated.
+ **/
+int32_t GCFSStructuredQuery_FieldFilter_Op_RawValue(GCFSStructuredQuery_FieldFilter *message);
+/**
+ * Sets the raw value of an @c GCFSStructuredQuery_FieldFilter's @c op property, allowing
+ * it to be set to a value that was not defined by the enum at the time the code
+ * was generated.
+ **/
+void SetGCFSStructuredQuery_FieldFilter_Op_RawValue(GCFSStructuredQuery_FieldFilter *message, int32_t value);
+
+#pragma mark - GCFSStructuredQuery_UnaryFilter
+
+typedef GPB_ENUM(GCFSStructuredQuery_UnaryFilter_FieldNumber) {
+ GCFSStructuredQuery_UnaryFilter_FieldNumber_Op = 1,
+ GCFSStructuredQuery_UnaryFilter_FieldNumber_Field = 2,
+};
+
+typedef GPB_ENUM(GCFSStructuredQuery_UnaryFilter_OperandType_OneOfCase) {
+ GCFSStructuredQuery_UnaryFilter_OperandType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSStructuredQuery_UnaryFilter_OperandType_OneOfCase_Field = 2,
+};
+
+/**
+ * A filter with a single operand.
+ **/
+@interface GCFSStructuredQuery_UnaryFilter : GPBMessage
+
+/** The unary operator to apply. */
+@property(nonatomic, readwrite) GCFSStructuredQuery_UnaryFilter_Operator op;
+
+/** The argument to the filter. */
+@property(nonatomic, readonly) GCFSStructuredQuery_UnaryFilter_OperandType_OneOfCase operandTypeOneOfCase;
+
+/** The field to which to apply the operator. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_FieldReference *field;
+
+@end
+
+/**
+ * Fetches the raw value of a @c GCFSStructuredQuery_UnaryFilter's @c op property, even
+ * if the value was not defined by the enum at the time the code was generated.
+ **/
+int32_t GCFSStructuredQuery_UnaryFilter_Op_RawValue(GCFSStructuredQuery_UnaryFilter *message);
+/**
+ * Sets the raw value of an @c GCFSStructuredQuery_UnaryFilter's @c op property, allowing
+ * it to be set to a value that was not defined by the enum at the time the code
+ * was generated.
+ **/
+void SetGCFSStructuredQuery_UnaryFilter_Op_RawValue(GCFSStructuredQuery_UnaryFilter *message, int32_t value);
+
+/**
+ * Clears whatever value was set for the oneof 'operandType'.
+ **/
+void GCFSStructuredQuery_UnaryFilter_ClearOperandTypeOneOfCase(GCFSStructuredQuery_UnaryFilter *message);
+
+#pragma mark - GCFSStructuredQuery_Order
+
+typedef GPB_ENUM(GCFSStructuredQuery_Order_FieldNumber) {
+ GCFSStructuredQuery_Order_FieldNumber_Field = 1,
+ GCFSStructuredQuery_Order_FieldNumber_Direction = 2,
+};
+
+/**
+ * An order on a field.
+ **/
+@interface GCFSStructuredQuery_Order : GPBMessage
+
+/** The field to order by. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_FieldReference *field;
+/** Test to see if @c field has been set. */
+@property(nonatomic, readwrite) BOOL hasField;
+
+/** The direction to order by. Defaults to `ASCENDING`. */
+@property(nonatomic, readwrite) GCFSStructuredQuery_Direction direction;
+
+@end
+
+/**
+ * Fetches the raw value of a @c GCFSStructuredQuery_Order's @c direction property, even
+ * if the value was not defined by the enum at the time the code was generated.
+ **/
+int32_t GCFSStructuredQuery_Order_Direction_RawValue(GCFSStructuredQuery_Order *message);
+/**
+ * Sets the raw value of an @c GCFSStructuredQuery_Order's @c direction property, allowing
+ * it to be set to a value that was not defined by the enum at the time the code
+ * was generated.
+ **/
+void SetGCFSStructuredQuery_Order_Direction_RawValue(GCFSStructuredQuery_Order *message, int32_t value);
+
+#pragma mark - GCFSStructuredQuery_FieldReference
+
+typedef GPB_ENUM(GCFSStructuredQuery_FieldReference_FieldNumber) {
+ GCFSStructuredQuery_FieldReference_FieldNumber_FieldPath = 2,
+};
+
+/**
+ * A reference to a field, such as `max(messages.time) as max_time`.
+ **/
+@interface GCFSStructuredQuery_FieldReference : GPBMessage
+
+@property(nonatomic, readwrite, copy, null_resettable) NSString *fieldPath;
+
+@end
+
+#pragma mark - GCFSStructuredQuery_Projection
+
+typedef GPB_ENUM(GCFSStructuredQuery_Projection_FieldNumber) {
+ GCFSStructuredQuery_Projection_FieldNumber_FieldsArray = 2,
+};
+
+/**
+ * The projection of document's fields to return.
+ **/
+@interface GCFSStructuredQuery_Projection : GPBMessage
+
+/**
+ * The fields to return.
+ *
+ * If empty, all fields are returned. To only return the name
+ * of the document, use `['__name__']`.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSStructuredQuery_FieldReference*> *fieldsArray;
+/** The number of items in @c fieldsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger fieldsArray_Count;
+
+@end
+
+#pragma mark - GCFSCursor
+
+typedef GPB_ENUM(GCFSCursor_FieldNumber) {
+ GCFSCursor_FieldNumber_ValuesArray = 1,
+ GCFSCursor_FieldNumber_Before = 2,
+};
+
+/**
+ * A position in a query result set.
+ **/
+@interface GCFSCursor : GPBMessage
+
+/**
+ * The values that represent a position, in the order they appear in
+ * the order by clause of a query.
+ *
+ * Can contain fewer values than specified in the order by clause.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSValue*> *valuesArray;
+/** The number of items in @c valuesArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger valuesArray_Count;
+
+/**
+ * If the position is just before or just after the given values, relative
+ * to the sort order defined by the query.
+ **/
+@property(nonatomic, readwrite) BOOL before;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.m
new file mode 100644
index 0000000..804a5d0
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.m
@@ -0,0 +1,907 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/query.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Wrappers.pbobjc.h>
+#else
+ #import "Wrappers.pbobjc.h"
+#endif
+
+ #import "Query.pbobjc.h"
+ #import "Annotations.pbobjc.h"
+ #import "Document.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#pragma clang diagnostic ignored "-Wdirect-ivar-access"
+
+#pragma mark - GCFSQueryRoot
+
+@implementation GCFSQueryRoot
+
+
+@end
+
+#pragma mark - GCFSQueryRoot_FileDescriptor
+
+static GPBFileDescriptor *GCFSQueryRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1"
+ objcPrefix:@"GCFS"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - GCFSStructuredQuery
+
+@implementation GCFSStructuredQuery
+
+@dynamic hasSelect, select;
+@dynamic fromArray, fromArray_Count;
+@dynamic hasWhere, where;
+@dynamic orderByArray, orderByArray_Count;
+@dynamic hasStartAt, startAt;
+@dynamic hasEndAt, endAt;
+@dynamic offset;
+@dynamic hasLimit, limit;
+
+typedef struct GCFSStructuredQuery__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t offset;
+ GCFSStructuredQuery_Projection *select;
+ NSMutableArray *fromArray;
+ GCFSStructuredQuery_Filter *where;
+ NSMutableArray *orderByArray;
+ GPBInt32Value *limit;
+ GCFSCursor *startAt;
+ GCFSCursor *endAt;
+} GCFSStructuredQuery__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "select",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_Projection),
+ .number = GCFSStructuredQuery_FieldNumber_Select,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, select),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "fromArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_CollectionSelector),
+ .number = GCFSStructuredQuery_FieldNumber_FromArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, fromArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "where",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_Filter),
+ .number = GCFSStructuredQuery_FieldNumber_Where,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, where),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "orderByArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_Order),
+ .number = GCFSStructuredQuery_FieldNumber_OrderByArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, orderByArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "limit",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBInt32Value),
+ .number = GCFSStructuredQuery_FieldNumber_Limit,
+ .hasIndex = 5,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, limit),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "offset",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSStructuredQuery_FieldNumber_Offset,
+ .hasIndex = 4,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, offset),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "startAt",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSCursor),
+ .number = GCFSStructuredQuery_FieldNumber_StartAt,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, startAt),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "endAt",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSCursor),
+ .number = GCFSStructuredQuery_FieldNumber_EndAt,
+ .hasIndex = 3,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, endAt),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSStructuredQuery__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - Enum GCFSStructuredQuery_Direction
+
+GPBEnumDescriptor *GCFSStructuredQuery_Direction_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "DirectionUnspecified\000Ascending\000Descendin"
+ "g\000";
+ static const int32_t values[] = {
+ GCFSStructuredQuery_Direction_DirectionUnspecified,
+ GCFSStructuredQuery_Direction_Ascending,
+ GCFSStructuredQuery_Direction_Descending,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSStructuredQuery_Direction)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GCFSStructuredQuery_Direction_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GCFSStructuredQuery_Direction_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GCFSStructuredQuery_Direction_DirectionUnspecified:
+ case GCFSStructuredQuery_Direction_Ascending:
+ case GCFSStructuredQuery_Direction_Descending:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GCFSStructuredQuery_CollectionSelector
+
+@implementation GCFSStructuredQuery_CollectionSelector
+
+@dynamic collectionId;
+@dynamic allDescendants;
+
+typedef struct GCFSStructuredQuery_CollectionSelector__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *collectionId;
+} GCFSStructuredQuery_CollectionSelector__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "collectionId",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSStructuredQuery_CollectionSelector_FieldNumber_CollectionId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_CollectionSelector__storage_, collectionId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "allDescendants",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSStructuredQuery_CollectionSelector_FieldNumber_AllDescendants,
+ .hasIndex = 1,
+ .offset = 2, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_CollectionSelector class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSStructuredQuery_CollectionSelector__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSStructuredQuery_Filter
+
+@implementation GCFSStructuredQuery_Filter
+
+@dynamic filterTypeOneOfCase;
+@dynamic compositeFilter;
+@dynamic fieldFilter;
+@dynamic unaryFilter;
+
+typedef struct GCFSStructuredQuery_Filter__storage_ {
+ uint32_t _has_storage_[2];
+ GCFSStructuredQuery_CompositeFilter *compositeFilter;
+ GCFSStructuredQuery_FieldFilter *fieldFilter;
+ GCFSStructuredQuery_UnaryFilter *unaryFilter;
+} GCFSStructuredQuery_Filter__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "compositeFilter",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_CompositeFilter),
+ .number = GCFSStructuredQuery_Filter_FieldNumber_CompositeFilter,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_Filter__storage_, compositeFilter),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "fieldFilter",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldFilter),
+ .number = GCFSStructuredQuery_Filter_FieldNumber_FieldFilter,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_Filter__storage_, fieldFilter),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "unaryFilter",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_UnaryFilter),
+ .number = GCFSStructuredQuery_Filter_FieldNumber_UnaryFilter,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_Filter__storage_, unaryFilter),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_Filter class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSStructuredQuery_Filter__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "filterType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSStructuredQuery_Filter_ClearFilterTypeOneOfCase(GCFSStructuredQuery_Filter *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSStructuredQuery_CompositeFilter
+
+@implementation GCFSStructuredQuery_CompositeFilter
+
+@dynamic op;
+@dynamic filtersArray, filtersArray_Count;
+
+typedef struct GCFSStructuredQuery_CompositeFilter__storage_ {
+ uint32_t _has_storage_[1];
+ GCFSStructuredQuery_CompositeFilter_Operator op;
+ NSMutableArray *filtersArray;
+} GCFSStructuredQuery_CompositeFilter__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "op",
+ .dataTypeSpecific.enumDescFunc = GCFSStructuredQuery_CompositeFilter_Operator_EnumDescriptor,
+ .number = GCFSStructuredQuery_CompositeFilter_FieldNumber_Op,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_CompositeFilter__storage_, op),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "filtersArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_Filter),
+ .number = GCFSStructuredQuery_CompositeFilter_FieldNumber_FiltersArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_CompositeFilter__storage_, filtersArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_CompositeFilter class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSStructuredQuery_CompositeFilter__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+int32_t GCFSStructuredQuery_CompositeFilter_Op_RawValue(GCFSStructuredQuery_CompositeFilter *message) {
+ GPBDescriptor *descriptor = [GCFSStructuredQuery_CompositeFilter descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_CompositeFilter_FieldNumber_Op];
+ return GPBGetMessageInt32Field(message, field);
+}
+
+void SetGCFSStructuredQuery_CompositeFilter_Op_RawValue(GCFSStructuredQuery_CompositeFilter *message, int32_t value) {
+ GPBDescriptor *descriptor = [GCFSStructuredQuery_CompositeFilter descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_CompositeFilter_FieldNumber_Op];
+ GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax);
+}
+
+#pragma mark - Enum GCFSStructuredQuery_CompositeFilter_Operator
+
+GPBEnumDescriptor *GCFSStructuredQuery_CompositeFilter_Operator_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "OperatorUnspecified\000And\000";
+ static const int32_t values[] = {
+ GCFSStructuredQuery_CompositeFilter_Operator_OperatorUnspecified,
+ GCFSStructuredQuery_CompositeFilter_Operator_And,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSStructuredQuery_CompositeFilter_Operator)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GCFSStructuredQuery_CompositeFilter_Operator_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GCFSStructuredQuery_CompositeFilter_Operator_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GCFSStructuredQuery_CompositeFilter_Operator_OperatorUnspecified:
+ case GCFSStructuredQuery_CompositeFilter_Operator_And:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GCFSStructuredQuery_FieldFilter
+
+@implementation GCFSStructuredQuery_FieldFilter
+
+@dynamic hasField, field;
+@dynamic op;
+@dynamic hasValue, value;
+
+typedef struct GCFSStructuredQuery_FieldFilter__storage_ {
+ uint32_t _has_storage_[1];
+ GCFSStructuredQuery_FieldFilter_Operator op;
+ GCFSStructuredQuery_FieldReference *field;
+ GCFSValue *value;
+} GCFSStructuredQuery_FieldFilter__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "field",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldReference),
+ .number = GCFSStructuredQuery_FieldFilter_FieldNumber_Field,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_FieldFilter__storage_, field),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "op",
+ .dataTypeSpecific.enumDescFunc = GCFSStructuredQuery_FieldFilter_Operator_EnumDescriptor,
+ .number = GCFSStructuredQuery_FieldFilter_FieldNumber_Op,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_FieldFilter__storage_, op),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "value",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue),
+ .number = GCFSStructuredQuery_FieldFilter_FieldNumber_Value,
+ .hasIndex = 2,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_FieldFilter__storage_, value),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_FieldFilter class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSStructuredQuery_FieldFilter__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+int32_t GCFSStructuredQuery_FieldFilter_Op_RawValue(GCFSStructuredQuery_FieldFilter *message) {
+ GPBDescriptor *descriptor = [GCFSStructuredQuery_FieldFilter descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_FieldFilter_FieldNumber_Op];
+ return GPBGetMessageInt32Field(message, field);
+}
+
+void SetGCFSStructuredQuery_FieldFilter_Op_RawValue(GCFSStructuredQuery_FieldFilter *message, int32_t value) {
+ GPBDescriptor *descriptor = [GCFSStructuredQuery_FieldFilter descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_FieldFilter_FieldNumber_Op];
+ GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax);
+}
+
+#pragma mark - Enum GCFSStructuredQuery_FieldFilter_Operator
+
+GPBEnumDescriptor *GCFSStructuredQuery_FieldFilter_Operator_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "OperatorUnspecified\000LessThan\000LessThanOrE"
+ "qual\000GreaterThan\000GreaterThanOrEqual\000Equa"
+ "l\000";
+ static const int32_t values[] = {
+ GCFSStructuredQuery_FieldFilter_Operator_OperatorUnspecified,
+ GCFSStructuredQuery_FieldFilter_Operator_LessThan,
+ GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual,
+ GCFSStructuredQuery_FieldFilter_Operator_GreaterThan,
+ GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual,
+ GCFSStructuredQuery_FieldFilter_Operator_Equal,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSStructuredQuery_FieldFilter_Operator)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GCFSStructuredQuery_FieldFilter_Operator_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GCFSStructuredQuery_FieldFilter_Operator_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GCFSStructuredQuery_FieldFilter_Operator_OperatorUnspecified:
+ case GCFSStructuredQuery_FieldFilter_Operator_LessThan:
+ case GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual:
+ case GCFSStructuredQuery_FieldFilter_Operator_GreaterThan:
+ case GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual:
+ case GCFSStructuredQuery_FieldFilter_Operator_Equal:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GCFSStructuredQuery_UnaryFilter
+
+@implementation GCFSStructuredQuery_UnaryFilter
+
+@dynamic operandTypeOneOfCase;
+@dynamic op;
+@dynamic field;
+
+typedef struct GCFSStructuredQuery_UnaryFilter__storage_ {
+ uint32_t _has_storage_[2];
+ GCFSStructuredQuery_UnaryFilter_Operator op;
+ GCFSStructuredQuery_FieldReference *field;
+} GCFSStructuredQuery_UnaryFilter__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "op",
+ .dataTypeSpecific.enumDescFunc = GCFSStructuredQuery_UnaryFilter_Operator_EnumDescriptor,
+ .number = GCFSStructuredQuery_UnaryFilter_FieldNumber_Op,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_UnaryFilter__storage_, op),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ {
+ .name = "field",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldReference),
+ .number = GCFSStructuredQuery_UnaryFilter_FieldNumber_Field,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_UnaryFilter__storage_, field),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_UnaryFilter class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSStructuredQuery_UnaryFilter__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "operandType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+int32_t GCFSStructuredQuery_UnaryFilter_Op_RawValue(GCFSStructuredQuery_UnaryFilter *message) {
+ GPBDescriptor *descriptor = [GCFSStructuredQuery_UnaryFilter descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_UnaryFilter_FieldNumber_Op];
+ return GPBGetMessageInt32Field(message, field);
+}
+
+void SetGCFSStructuredQuery_UnaryFilter_Op_RawValue(GCFSStructuredQuery_UnaryFilter *message, int32_t value) {
+ GPBDescriptor *descriptor = [GCFSStructuredQuery_UnaryFilter descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_UnaryFilter_FieldNumber_Op];
+ GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax);
+}
+
+void GCFSStructuredQuery_UnaryFilter_ClearOperandTypeOneOfCase(GCFSStructuredQuery_UnaryFilter *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - Enum GCFSStructuredQuery_UnaryFilter_Operator
+
+GPBEnumDescriptor *GCFSStructuredQuery_UnaryFilter_Operator_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "OperatorUnspecified\000IsNan\000IsNull\000";
+ static const int32_t values[] = {
+ GCFSStructuredQuery_UnaryFilter_Operator_OperatorUnspecified,
+ GCFSStructuredQuery_UnaryFilter_Operator_IsNan,
+ GCFSStructuredQuery_UnaryFilter_Operator_IsNull,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSStructuredQuery_UnaryFilter_Operator)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GCFSStructuredQuery_UnaryFilter_Operator_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GCFSStructuredQuery_UnaryFilter_Operator_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GCFSStructuredQuery_UnaryFilter_Operator_OperatorUnspecified:
+ case GCFSStructuredQuery_UnaryFilter_Operator_IsNan:
+ case GCFSStructuredQuery_UnaryFilter_Operator_IsNull:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GCFSStructuredQuery_Order
+
+@implementation GCFSStructuredQuery_Order
+
+@dynamic hasField, field;
+@dynamic direction;
+
+typedef struct GCFSStructuredQuery_Order__storage_ {
+ uint32_t _has_storage_[1];
+ GCFSStructuredQuery_Direction direction;
+ GCFSStructuredQuery_FieldReference *field;
+} GCFSStructuredQuery_Order__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "field",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldReference),
+ .number = GCFSStructuredQuery_Order_FieldNumber_Field,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_Order__storage_, field),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "direction",
+ .dataTypeSpecific.enumDescFunc = GCFSStructuredQuery_Direction_EnumDescriptor,
+ .number = GCFSStructuredQuery_Order_FieldNumber_Direction,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_Order__storage_, direction),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_Order class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSStructuredQuery_Order__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+int32_t GCFSStructuredQuery_Order_Direction_RawValue(GCFSStructuredQuery_Order *message) {
+ GPBDescriptor *descriptor = [GCFSStructuredQuery_Order descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_Order_FieldNumber_Direction];
+ return GPBGetMessageInt32Field(message, field);
+}
+
+void SetGCFSStructuredQuery_Order_Direction_RawValue(GCFSStructuredQuery_Order *message, int32_t value) {
+ GPBDescriptor *descriptor = [GCFSStructuredQuery_Order descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_Order_FieldNumber_Direction];
+ GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax);
+}
+
+#pragma mark - GCFSStructuredQuery_FieldReference
+
+@implementation GCFSStructuredQuery_FieldReference
+
+@dynamic fieldPath;
+
+typedef struct GCFSStructuredQuery_FieldReference__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *fieldPath;
+} GCFSStructuredQuery_FieldReference__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "fieldPath",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSStructuredQuery_FieldReference_FieldNumber_FieldPath,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_FieldReference__storage_, fieldPath),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_FieldReference class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSStructuredQuery_FieldReference__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSStructuredQuery_Projection
+
+@implementation GCFSStructuredQuery_Projection
+
+@dynamic fieldsArray, fieldsArray_Count;
+
+typedef struct GCFSStructuredQuery_Projection__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *fieldsArray;
+} GCFSStructuredQuery_Projection__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "fieldsArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldReference),
+ .number = GCFSStructuredQuery_Projection_FieldNumber_FieldsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSStructuredQuery_Projection__storage_, fieldsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_Projection class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSStructuredQuery_Projection__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSCursor
+
+@implementation GCFSCursor
+
+@dynamic valuesArray, valuesArray_Count;
+@dynamic before;
+
+typedef struct GCFSCursor__storage_ {
+ uint32_t _has_storage_[1];
+ NSMutableArray *valuesArray;
+} GCFSCursor__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "valuesArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue),
+ .number = GCFSCursor_FieldNumber_ValuesArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSCursor__storage_, valuesArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "before",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSCursor_FieldNumber_Before,
+ .hasIndex = 0,
+ .offset = 1, // Stored in _has_storage_ to save space.
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeBool,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSCursor class]
+ rootClass:[GCFSQueryRoot class]
+ file:GCFSQueryRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSCursor__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h
new file mode 100644
index 0000000..c3c4498
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h
@@ -0,0 +1,432 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/write.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GCFSDocument;
+@class GCFSDocumentMask;
+@class GCFSDocumentTransform;
+@class GCFSDocumentTransform_FieldTransform;
+@class GCFSPrecondition;
+@class GCFSValue;
+@class GPBTimestamp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - Enum GCFSDocumentTransform_FieldTransform_ServerValue
+
+/** A value that is calculated by the server. */
+typedef GPB_ENUM(GCFSDocumentTransform_FieldTransform_ServerValue) {
+ /**
+ * Value used if any message's field encounters a value that is not defined
+ * by this enum. The message will also have C functions to get/set the rawValue
+ * of the field.
+ **/
+ GCFSDocumentTransform_FieldTransform_ServerValue_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue,
+ /** Unspecified. This value must not be used. */
+ GCFSDocumentTransform_FieldTransform_ServerValue_ServerValueUnspecified = 0,
+
+ /** The time at which the server processed the request. */
+ GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime = 1,
+};
+
+GPBEnumDescriptor *GCFSDocumentTransform_FieldTransform_ServerValue_EnumDescriptor(void);
+
+/**
+ * Checks to see if the given value is defined by the enum or was not known at
+ * the time this source was generated.
+ **/
+BOOL GCFSDocumentTransform_FieldTransform_ServerValue_IsValidValue(int32_t value);
+
+#pragma mark - GCFSWriteRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GCFSWriteRoot : GPBRootObject
+@end
+
+#pragma mark - GCFSWrite
+
+typedef GPB_ENUM(GCFSWrite_FieldNumber) {
+ GCFSWrite_FieldNumber_Update = 1,
+ GCFSWrite_FieldNumber_Delete_p = 2,
+ GCFSWrite_FieldNumber_UpdateMask = 3,
+ GCFSWrite_FieldNumber_CurrentDocument = 4,
+ GCFSWrite_FieldNumber_Verify = 5,
+ GCFSWrite_FieldNumber_Transform = 6,
+};
+
+typedef GPB_ENUM(GCFSWrite_Operation_OneOfCase) {
+ GCFSWrite_Operation_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSWrite_Operation_OneOfCase_Update = 1,
+ GCFSWrite_Operation_OneOfCase_Delete_p = 2,
+ GCFSWrite_Operation_OneOfCase_Verify = 5,
+ GCFSWrite_Operation_OneOfCase_Transform = 6,
+};
+
+/**
+ * A write on a document.
+ **/
+@interface GCFSWrite : GPBMessage
+
+/** The operation to execute. */
+@property(nonatomic, readonly) GCFSWrite_Operation_OneOfCase operationOneOfCase;
+
+/** A document to write. */
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *update;
+
+/**
+ * A document name to delete. In the format:
+ * `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *delete_p;
+
+/**
+ * The name of a document on which to verify the `current_document`
+ * precondition.
+ * This only requires read access to the document.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *verify;
+
+/**
+ * Applies a tranformation to a document.
+ * At most one `transform` per document is allowed in a given request.
+ * An `update` cannot follow a `transform` on the same document in a given
+ * request.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentTransform *transform;
+
+/**
+ * The fields to update in this write.
+ *
+ * This field can be set only when the operation is `update`.
+ * None of the field paths in the mask may contain a reserved name.
+ * If the document exists on the server and has fields not referenced in the
+ * mask, they are left unchanged.
+ * Fields referenced in the mask, but not present in the input document, are
+ * deleted from the document on the server.
+ * The field paths in this mask must not contain a reserved field name.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *updateMask;
+/** Test to see if @c updateMask has been set. */
+@property(nonatomic, readwrite) BOOL hasUpdateMask;
+
+/**
+ * An optional precondition on the document.
+ *
+ * The write will fail if this is set and not met by the target document.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSPrecondition *currentDocument;
+/** Test to see if @c currentDocument has been set. */
+@property(nonatomic, readwrite) BOOL hasCurrentDocument;
+
+@end
+
+/**
+ * Clears whatever value was set for the oneof 'operation'.
+ **/
+void GCFSWrite_ClearOperationOneOfCase(GCFSWrite *message);
+
+#pragma mark - GCFSDocumentTransform
+
+typedef GPB_ENUM(GCFSDocumentTransform_FieldNumber) {
+ GCFSDocumentTransform_FieldNumber_Document = 1,
+ GCFSDocumentTransform_FieldNumber_FieldTransformsArray = 2,
+};
+
+/**
+ * A transformation of a document.
+ **/
+@interface GCFSDocumentTransform : GPBMessage
+
+/** The name of the document to transform. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *document;
+
+/**
+ * The list of transformations to apply to the fields of the document, in
+ * order.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSDocumentTransform_FieldTransform*> *fieldTransformsArray;
+/** The number of items in @c fieldTransformsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger fieldTransformsArray_Count;
+
+@end
+
+#pragma mark - GCFSDocumentTransform_FieldTransform
+
+typedef GPB_ENUM(GCFSDocumentTransform_FieldTransform_FieldNumber) {
+ GCFSDocumentTransform_FieldTransform_FieldNumber_FieldPath = 1,
+ GCFSDocumentTransform_FieldTransform_FieldNumber_SetToServerValue = 2,
+};
+
+typedef GPB_ENUM(GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase) {
+ GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase_GPBUnsetOneOfCase = 0,
+ GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase_SetToServerValue = 2,
+};
+
+/**
+ * A transformation of a field of the document.
+ **/
+@interface GCFSDocumentTransform_FieldTransform : GPBMessage
+
+/**
+ * The path of the field. See [Document.fields][google.firestore.v1beta1.Document.fields] for the field path syntax
+ * reference.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *fieldPath;
+
+/** The transformation to apply on the field. */
+@property(nonatomic, readonly) GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase transformTypeOneOfCase;
+
+/** Sets the field to the given server value. */
+@property(nonatomic, readwrite) GCFSDocumentTransform_FieldTransform_ServerValue setToServerValue;
+
+@end
+
+/**
+ * Fetches the raw value of a @c GCFSDocumentTransform_FieldTransform's @c setToServerValue property, even
+ * if the value was not defined by the enum at the time the code was generated.
+ **/
+int32_t GCFSDocumentTransform_FieldTransform_SetToServerValue_RawValue(GCFSDocumentTransform_FieldTransform *message);
+/**
+ * Sets the raw value of an @c GCFSDocumentTransform_FieldTransform's @c setToServerValue property, allowing
+ * it to be set to a value that was not defined by the enum at the time the code
+ * was generated.
+ **/
+void SetGCFSDocumentTransform_FieldTransform_SetToServerValue_RawValue(GCFSDocumentTransform_FieldTransform *message, int32_t value);
+
+/**
+ * Clears whatever value was set for the oneof 'transformType'.
+ **/
+void GCFSDocumentTransform_FieldTransform_ClearTransformTypeOneOfCase(GCFSDocumentTransform_FieldTransform *message);
+
+#pragma mark - GCFSWriteResult
+
+typedef GPB_ENUM(GCFSWriteResult_FieldNumber) {
+ GCFSWriteResult_FieldNumber_UpdateTime = 1,
+ GCFSWriteResult_FieldNumber_TransformResultsArray = 2,
+};
+
+/**
+ * The result of applying a write.
+ **/
+@interface GCFSWriteResult : GPBMessage
+
+/**
+ * The last update time of the document after applying the write. Not set
+ * after a `delete`.
+ *
+ * If the write did not actually change the document, this will be the
+ * previous update_time.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *updateTime;
+/** Test to see if @c updateTime has been set. */
+@property(nonatomic, readwrite) BOOL hasUpdateTime;
+
+/**
+ * The results of applying each [DocumentTransform.FieldTransform][google.firestore.v1beta1.DocumentTransform.FieldTransform], in the
+ * same order.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GCFSValue*> *transformResultsArray;
+/** The number of items in @c transformResultsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger transformResultsArray_Count;
+
+@end
+
+#pragma mark - GCFSDocumentChange
+
+typedef GPB_ENUM(GCFSDocumentChange_FieldNumber) {
+ GCFSDocumentChange_FieldNumber_Document = 1,
+ GCFSDocumentChange_FieldNumber_TargetIdsArray = 5,
+ GCFSDocumentChange_FieldNumber_RemovedTargetIdsArray = 6,
+};
+
+/**
+ * A [Document][google.firestore.v1beta1.Document] has changed.
+ *
+ * May be the result of multiple [writes][google.firestore.v1beta1.Write], including deletes, that
+ * ultimately resulted in a new value for the [Document][google.firestore.v1beta1.Document].
+ *
+ * Multiple [DocumentChange][google.firestore.v1beta1.DocumentChange] messages may be returned for the same logical
+ * change, if multiple targets are affected.
+ **/
+@interface GCFSDocumentChange : GPBMessage
+
+/**
+ * The new state of the [Document][google.firestore.v1beta1.Document].
+ *
+ * If `mask` is set, contains only fields that were updated or added.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document;
+/** Test to see if @c document has been set. */
+@property(nonatomic, readwrite) BOOL hasDocument;
+
+/** A set of target IDs of targets that match this document. */
+@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *targetIdsArray;
+/** The number of items in @c targetIdsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger targetIdsArray_Count;
+
+/** A set of target IDs for targets that no longer match this document. */
+@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *removedTargetIdsArray;
+/** The number of items in @c removedTargetIdsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger removedTargetIdsArray_Count;
+
+@end
+
+#pragma mark - GCFSDocumentDelete
+
+typedef GPB_ENUM(GCFSDocumentDelete_FieldNumber) {
+ GCFSDocumentDelete_FieldNumber_Document = 1,
+ GCFSDocumentDelete_FieldNumber_ReadTime = 4,
+ GCFSDocumentDelete_FieldNumber_RemovedTargetIdsArray = 6,
+};
+
+/**
+ * A [Document][google.firestore.v1beta1.Document] has been deleted.
+ *
+ * May be the result of multiple [writes][google.firestore.v1beta1.Write], including updates, the
+ * last of which deleted the [Document][google.firestore.v1beta1.Document].
+ *
+ * Multiple [DocumentDelete][google.firestore.v1beta1.DocumentDelete] messages may be returned for the same logical
+ * delete, if multiple targets are affected.
+ **/
+@interface GCFSDocumentDelete : GPBMessage
+
+/** The resource name of the [Document][google.firestore.v1beta1.Document] that was deleted. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *document;
+
+/** A set of target IDs for targets that previously matched this entity. */
+@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *removedTargetIdsArray;
+/** The number of items in @c removedTargetIdsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger removedTargetIdsArray_Count;
+
+/**
+ * The read timestamp at which the delete was observed.
+ *
+ * Greater or equal to the `commit_time` of the delete.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+/** Test to see if @c readTime has been set. */
+@property(nonatomic, readwrite) BOOL hasReadTime;
+
+@end
+
+#pragma mark - GCFSDocumentRemove
+
+typedef GPB_ENUM(GCFSDocumentRemove_FieldNumber) {
+ GCFSDocumentRemove_FieldNumber_Document = 1,
+ GCFSDocumentRemove_FieldNumber_RemovedTargetIdsArray = 2,
+ GCFSDocumentRemove_FieldNumber_ReadTime = 4,
+};
+
+/**
+ * A [Document][google.firestore.v1beta1.Document] has been removed from the view of the targets.
+ *
+ * Sent if the document is no longer relevant to a target and is out of view.
+ * Can be sent instead of a DocumentDelete or a DocumentChange if the server
+ * can not send the new value of the document.
+ *
+ * Multiple [DocumentRemove][google.firestore.v1beta1.DocumentRemove] messages may be returned for the same logical
+ * write or delete, if multiple targets are affected.
+ **/
+@interface GCFSDocumentRemove : GPBMessage
+
+/** The resource name of the [Document][google.firestore.v1beta1.Document] that has gone out of view. */
+@property(nonatomic, readwrite, copy, null_resettable) NSString *document;
+
+/** A set of target IDs for targets that previously matched this document. */
+@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *removedTargetIdsArray;
+/** The number of items in @c removedTargetIdsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger removedTargetIdsArray_Count;
+
+/**
+ * The read timestamp at which the remove was observed.
+ *
+ * Greater or equal to the `commit_time` of the change/delete/remove.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime;
+/** Test to see if @c readTime has been set. */
+@property(nonatomic, readwrite) BOOL hasReadTime;
+
+@end
+
+#pragma mark - GCFSExistenceFilter
+
+typedef GPB_ENUM(GCFSExistenceFilter_FieldNumber) {
+ GCFSExistenceFilter_FieldNumber_TargetId = 1,
+ GCFSExistenceFilter_FieldNumber_Count = 2,
+};
+
+/**
+ * A digest of all the documents that match a given target.
+ **/
+@interface GCFSExistenceFilter : GPBMessage
+
+/** The target ID to which this filter applies. */
+@property(nonatomic, readwrite) int32_t targetId;
+
+/**
+ * The total count of documents that match [target_id][google.firestore.v1beta1.ExistenceFilter.target_id].
+ *
+ * If different from the count of documents in the client that match, the
+ * client must manually determine which documents no longer match the target.
+ **/
+@property(nonatomic, readwrite) int32_t count;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.m
new file mode 100644
index 0000000..e6fd0f4
--- /dev/null
+++ b/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.m
@@ -0,0 +1,653 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/firestore/v1beta1/write.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Timestamp.pbobjc.h>
+#else
+ #import "Timestamp.pbobjc.h"
+#endif
+
+ #import "Write.pbobjc.h"
+ #import "Annotations.pbobjc.h"
+ #import "Common.pbobjc.h"
+ #import "Document.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#pragma clang diagnostic ignored "-Wdirect-ivar-access"
+
+#pragma mark - GCFSWriteRoot
+
+@implementation GCFSWriteRoot
+
+
+@end
+
+#pragma mark - GCFSWriteRoot_FileDescriptor
+
+static GPBFileDescriptor *GCFSWriteRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1"
+ objcPrefix:@"GCFS"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - GCFSWrite
+
+@implementation GCFSWrite
+
+@dynamic operationOneOfCase;
+@dynamic update;
+@dynamic delete_p;
+@dynamic verify;
+@dynamic transform;
+@dynamic hasUpdateMask, updateMask;
+@dynamic hasCurrentDocument, currentDocument;
+
+typedef struct GCFSWrite__storage_ {
+ uint32_t _has_storage_[2];
+ GCFSDocument *update;
+ NSString *delete_p;
+ GCFSDocumentMask *updateMask;
+ GCFSPrecondition *currentDocument;
+ NSString *verify;
+ GCFSDocumentTransform *transform;
+} GCFSWrite__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "update",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument),
+ .number = GCFSWrite_FieldNumber_Update,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSWrite__storage_, update),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "delete_p",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSWrite_FieldNumber_Delete_p,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSWrite__storage_, delete_p),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "updateMask",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask),
+ .number = GCFSWrite_FieldNumber_UpdateMask,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSWrite__storage_, updateMask),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "currentDocument",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSPrecondition),
+ .number = GCFSWrite_FieldNumber_CurrentDocument,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSWrite__storage_, currentDocument),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "verify",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSWrite_FieldNumber_Verify,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSWrite__storage_, verify),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "transform",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentTransform),
+ .number = GCFSWrite_FieldNumber_Transform,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSWrite__storage_, transform),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSWrite class]
+ rootClass:[GCFSWriteRoot class]
+ file:GCFSWriteRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSWrite__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "operation",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+void GCFSWrite_ClearOperationOneOfCase(GCFSWrite *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - GCFSDocumentTransform
+
+@implementation GCFSDocumentTransform
+
+@dynamic document;
+@dynamic fieldTransformsArray, fieldTransformsArray_Count;
+
+typedef struct GCFSDocumentTransform__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *document;
+ NSMutableArray *fieldTransformsArray;
+} GCFSDocumentTransform__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "document",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocumentTransform_FieldNumber_Document,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSDocumentTransform__storage_, document),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "fieldTransformsArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentTransform_FieldTransform),
+ .number = GCFSDocumentTransform_FieldNumber_FieldTransformsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSDocumentTransform__storage_, fieldTransformsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSDocumentTransform class]
+ rootClass:[GCFSWriteRoot class]
+ file:GCFSWriteRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSDocumentTransform__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSDocumentTransform_FieldTransform
+
+@implementation GCFSDocumentTransform_FieldTransform
+
+@dynamic transformTypeOneOfCase;
+@dynamic fieldPath;
+@dynamic setToServerValue;
+
+typedef struct GCFSDocumentTransform_FieldTransform__storage_ {
+ uint32_t _has_storage_[2];
+ GCFSDocumentTransform_FieldTransform_ServerValue setToServerValue;
+ NSString *fieldPath;
+} GCFSDocumentTransform_FieldTransform__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "fieldPath",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocumentTransform_FieldTransform_FieldNumber_FieldPath,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSDocumentTransform_FieldTransform__storage_, fieldPath),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "setToServerValue",
+ .dataTypeSpecific.enumDescFunc = GCFSDocumentTransform_FieldTransform_ServerValue_EnumDescriptor,
+ .number = GCFSDocumentTransform_FieldTransform_FieldNumber_SetToServerValue,
+ .hasIndex = -1,
+ .offset = (uint32_t)offsetof(GCFSDocumentTransform_FieldTransform__storage_, setToServerValue),
+ .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor),
+ .dataType = GPBDataTypeEnum,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSDocumentTransform_FieldTransform class]
+ rootClass:[GCFSWriteRoot class]
+ file:GCFSWriteRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSDocumentTransform_FieldTransform__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ static const char *oneofs[] = {
+ "transformType",
+ };
+ [localDescriptor setupOneofs:oneofs
+ count:(uint32_t)(sizeof(oneofs) / sizeof(char*))
+ firstHasIndex:-1];
+ [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSDocumentTransform)];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+int32_t GCFSDocumentTransform_FieldTransform_SetToServerValue_RawValue(GCFSDocumentTransform_FieldTransform *message) {
+ GPBDescriptor *descriptor = [GCFSDocumentTransform_FieldTransform descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSDocumentTransform_FieldTransform_FieldNumber_SetToServerValue];
+ return GPBGetMessageInt32Field(message, field);
+}
+
+void SetGCFSDocumentTransform_FieldTransform_SetToServerValue_RawValue(GCFSDocumentTransform_FieldTransform *message, int32_t value) {
+ GPBDescriptor *descriptor = [GCFSDocumentTransform_FieldTransform descriptor];
+ GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSDocumentTransform_FieldTransform_FieldNumber_SetToServerValue];
+ GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax);
+}
+
+void GCFSDocumentTransform_FieldTransform_ClearTransformTypeOneOfCase(GCFSDocumentTransform_FieldTransform *message) {
+ GPBDescriptor *descriptor = [message descriptor];
+ GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0];
+ GPBMaybeClearOneof(message, oneof, -1, 0);
+}
+#pragma mark - Enum GCFSDocumentTransform_FieldTransform_ServerValue
+
+GPBEnumDescriptor *GCFSDocumentTransform_FieldTransform_ServerValue_EnumDescriptor(void) {
+ static GPBEnumDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ static const char *valueNames =
+ "ServerValueUnspecified\000RequestTime\000";
+ static const int32_t values[] = {
+ GCFSDocumentTransform_FieldTransform_ServerValue_ServerValueUnspecified,
+ GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime,
+ };
+ GPBEnumDescriptor *worker =
+ [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSDocumentTransform_FieldTransform_ServerValue)
+ valueNames:valueNames
+ values:values
+ count:(uint32_t)(sizeof(values) / sizeof(int32_t))
+ enumVerifier:GCFSDocumentTransform_FieldTransform_ServerValue_IsValidValue];
+ if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) {
+ [worker release];
+ }
+ }
+ return descriptor;
+}
+
+BOOL GCFSDocumentTransform_FieldTransform_ServerValue_IsValidValue(int32_t value__) {
+ switch (value__) {
+ case GCFSDocumentTransform_FieldTransform_ServerValue_ServerValueUnspecified:
+ case GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+#pragma mark - GCFSWriteResult
+
+@implementation GCFSWriteResult
+
+@dynamic hasUpdateTime, updateTime;
+@dynamic transformResultsArray, transformResultsArray_Count;
+
+typedef struct GCFSWriteResult__storage_ {
+ uint32_t _has_storage_[1];
+ GPBTimestamp *updateTime;
+ NSMutableArray *transformResultsArray;
+} GCFSWriteResult__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "updateTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSWriteResult_FieldNumber_UpdateTime,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSWriteResult__storage_, updateTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "transformResultsArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue),
+ .number = GCFSWriteResult_FieldNumber_TransformResultsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSWriteResult__storage_, transformResultsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSWriteResult class]
+ rootClass:[GCFSWriteRoot class]
+ file:GCFSWriteRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSWriteResult__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSDocumentChange
+
+@implementation GCFSDocumentChange
+
+@dynamic hasDocument, document;
+@dynamic targetIdsArray, targetIdsArray_Count;
+@dynamic removedTargetIdsArray, removedTargetIdsArray_Count;
+
+typedef struct GCFSDocumentChange__storage_ {
+ uint32_t _has_storage_[1];
+ GCFSDocument *document;
+ GPBInt32Array *targetIdsArray;
+ GPBInt32Array *removedTargetIdsArray;
+} GCFSDocumentChange__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "document",
+ .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument),
+ .number = GCFSDocumentChange_FieldNumber_Document,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSDocumentChange__storage_, document),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "targetIdsArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocumentChange_FieldNumber_TargetIdsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSDocumentChange__storage_, targetIdsArray),
+ .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked),
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "removedTargetIdsArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocumentChange_FieldNumber_RemovedTargetIdsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSDocumentChange__storage_, removedTargetIdsArray),
+ .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked),
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSDocumentChange class]
+ rootClass:[GCFSWriteRoot class]
+ file:GCFSWriteRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSDocumentChange__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSDocumentDelete
+
+@implementation GCFSDocumentDelete
+
+@dynamic document;
+@dynamic removedTargetIdsArray, removedTargetIdsArray_Count;
+@dynamic hasReadTime, readTime;
+
+typedef struct GCFSDocumentDelete__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *document;
+ GPBTimestamp *readTime;
+ GPBInt32Array *removedTargetIdsArray;
+} GCFSDocumentDelete__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "document",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocumentDelete_FieldNumber_Document,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSDocumentDelete__storage_, document),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSDocumentDelete_FieldNumber_ReadTime,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSDocumentDelete__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ {
+ .name = "removedTargetIdsArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocumentDelete_FieldNumber_RemovedTargetIdsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSDocumentDelete__storage_, removedTargetIdsArray),
+ .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked),
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSDocumentDelete class]
+ rootClass:[GCFSWriteRoot class]
+ file:GCFSWriteRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSDocumentDelete__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSDocumentRemove
+
+@implementation GCFSDocumentRemove
+
+@dynamic document;
+@dynamic removedTargetIdsArray, removedTargetIdsArray_Count;
+@dynamic hasReadTime, readTime;
+
+typedef struct GCFSDocumentRemove__storage_ {
+ uint32_t _has_storage_[1];
+ NSString *document;
+ GPBInt32Array *removedTargetIdsArray;
+ GPBTimestamp *readTime;
+} GCFSDocumentRemove__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "document",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocumentRemove_FieldNumber_Document,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSDocumentRemove__storage_, document),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "removedTargetIdsArray",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSDocumentRemove_FieldNumber_RemovedTargetIdsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(GCFSDocumentRemove__storage_, removedTargetIdsArray),
+ .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked),
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "readTime",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp),
+ .number = GCFSDocumentRemove_FieldNumber_ReadTime,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSDocumentRemove__storage_, readTime),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSDocumentRemove class]
+ rootClass:[GCFSWriteRoot class]
+ file:GCFSWriteRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSDocumentRemove__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+#pragma mark - GCFSExistenceFilter
+
+@implementation GCFSExistenceFilter
+
+@dynamic targetId;
+@dynamic count;
+
+typedef struct GCFSExistenceFilter__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t targetId;
+ int32_t count;
+} GCFSExistenceFilter__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "targetId",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSExistenceFilter_FieldNumber_TargetId,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GCFSExistenceFilter__storage_, targetId),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "count",
+ .dataTypeSpecific.className = NULL,
+ .number = GCFSExistenceFilter_FieldNumber_Count,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GCFSExistenceFilter__storage_, count),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GCFSExistenceFilter class]
+ rootClass:[GCFSWriteRoot class]
+ file:GCFSWriteRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GCFSExistenceFilter__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/rpc/Status.pbobjc.h b/Firestore/Protos/objc/google/rpc/Status.pbobjc.h
new file mode 100644
index 0000000..86c21c7
--- /dev/null
+++ b/Firestore/Protos/objc/google/rpc/Status.pbobjc.h
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/rpc/status.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+@class GPBAny;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - RPCStatusRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface RPCStatusRoot : GPBRootObject
+@end
+
+#pragma mark - RPCStatus
+
+typedef GPB_ENUM(RPCStatus_FieldNumber) {
+ RPCStatus_FieldNumber_Code = 1,
+ RPCStatus_FieldNumber_Message = 2,
+ RPCStatus_FieldNumber_DetailsArray = 3,
+};
+
+/**
+ * The `Status` type defines a logical error model that is suitable for different
+ * programming environments, including REST APIs and RPC APIs. It is used by
+ * [gRPC](https://github.com/grpc). The error model is designed to be:
+ *
+ * - Simple to use and understand for most users
+ * - Flexible enough to meet unexpected needs
+ *
+ * # Overview
+ *
+ * The `Status` message contains three pieces of data: error code, error message,
+ * and error details. The error code should be an enum value of
+ * [google.rpc.Code][google.rpc.Code], but it may accept additional error codes if needed. The
+ * error message should be a developer-facing English message that helps
+ * developers *understand* and *resolve* the error. If a localized user-facing
+ * error message is needed, put the localized message in the error details or
+ * localize it in the client. The optional error details may contain arbitrary
+ * information about the error. There is a predefined set of error detail types
+ * in the package `google.rpc` that can be used for common error conditions.
+ *
+ * # Language mapping
+ *
+ * The `Status` message is the logical representation of the error model, but it
+ * is not necessarily the actual wire format. When the `Status` message is
+ * exposed in different client libraries and different wire protocols, it can be
+ * mapped differently. For example, it will likely be mapped to some exceptions
+ * in Java, but more likely mapped to some error codes in C.
+ *
+ * # Other uses
+ *
+ * The error model and the `Status` message can be used in a variety of
+ * environments, either with or without APIs, to provide a
+ * consistent developer experience across different environments.
+ *
+ * Example uses of this error model include:
+ *
+ * - Partial errors. If a service needs to return partial errors to the client,
+ * it may embed the `Status` in the normal response to indicate the partial
+ * errors.
+ *
+ * - Workflow errors. A typical workflow has multiple steps. Each step may
+ * have a `Status` message for error reporting.
+ *
+ * - Batch operations. If a client uses batch request and batch response, the
+ * `Status` message should be used directly inside batch response, one for
+ * each error sub-response.
+ *
+ * - Asynchronous operations. If an API call embeds asynchronous operation
+ * results in its response, the status of those operations should be
+ * represented directly using the `Status` message.
+ *
+ * - Logging. If some API errors are stored in logs, the message `Status` could
+ * be used directly after any stripping needed for security/privacy reasons.
+ **/
+@interface RPCStatus : GPBMessage
+
+/** The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. */
+@property(nonatomic, readwrite) int32_t code;
+
+/**
+ * A developer-facing error message, which should be in English. Any
+ * user-facing error message should be localized and sent in the
+ * [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
+ **/
+@property(nonatomic, readwrite, copy, null_resettable) NSString *message;
+
+/**
+ * A list of messages that carry the error details. There is a common set of
+ * message types for APIs to use.
+ **/
+@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray<GPBAny*> *detailsArray;
+/** The number of items in @c detailsArray without causing the array to be created. */
+@property(nonatomic, readonly) NSUInteger detailsArray_Count;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/rpc/Status.pbobjc.m b/Firestore/Protos/objc/google/rpc/Status.pbobjc.m
new file mode 100644
index 0000000..831073c
--- /dev/null
+++ b/Firestore/Protos/objc/google/rpc/Status.pbobjc.m
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/rpc/status.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/Any.pbobjc.h>
+#else
+ #import "Any.pbobjc.h"
+#endif
+
+ #import "Status.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+#pragma mark - RPCStatusRoot
+
+@implementation RPCStatusRoot
+
+// No extensions in the file and none of the imports (direct or indirect)
+// defined extensions, so no need to generate +extensionRegistry.
+
+@end
+
+#pragma mark - RPCStatusRoot_FileDescriptor
+
+static GPBFileDescriptor *RPCStatusRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.rpc"
+ objcPrefix:@"RPC"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - RPCStatus
+
+@implementation RPCStatus
+
+@dynamic code;
+@dynamic message;
+@dynamic detailsArray, detailsArray_Count;
+
+typedef struct RPCStatus__storage_ {
+ uint32_t _has_storage_[1];
+ int32_t code;
+ NSString *message;
+ NSMutableArray *detailsArray;
+} RPCStatus__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "code",
+ .dataTypeSpecific.className = NULL,
+ .number = RPCStatus_FieldNumber_Code,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(RPCStatus__storage_, code),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeInt32,
+ },
+ {
+ .name = "message",
+ .dataTypeSpecific.className = NULL,
+ .number = RPCStatus_FieldNumber_Message,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(RPCStatus__storage_, message),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeString,
+ },
+ {
+ .name = "detailsArray",
+ .dataTypeSpecific.className = GPBStringifySymbol(GPBAny),
+ .number = RPCStatus_FieldNumber_DetailsArray,
+ .hasIndex = GPBNoHasBit,
+ .offset = (uint32_t)offsetof(RPCStatus__storage_, detailsArray),
+ .flags = GPBFieldRepeated,
+ .dataType = GPBDataTypeMessage,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[RPCStatus class]
+ rootClass:[RPCStatusRoot class]
+ file:RPCStatusRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(RPCStatus__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/type/Latlng.pbobjc.h b/Firestore/Protos/objc/google/type/Latlng.pbobjc.h
new file mode 100644
index 0000000..0af137f
--- /dev/null
+++ b/Firestore/Protos/objc/google/type/Latlng.pbobjc.h
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/type/latlng.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers.h>
+#else
+ #import "GPBProtocolBuffers.h"
+#endif
+
+#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002
+#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION
+#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources.
+#endif
+
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+CF_EXTERN_C_BEGIN
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - GTPLatlngRoot
+
+/**
+ * Exposes the extension registry for this file.
+ *
+ * The base class provides:
+ * @code
+ * + (GPBExtensionRegistry *)extensionRegistry;
+ * @endcode
+ * which is a @c GPBExtensionRegistry that includes all the extensions defined by
+ * this file and all files that it depends on.
+ **/
+@interface GTPLatlngRoot : GPBRootObject
+@end
+
+#pragma mark - GTPLatLng
+
+typedef GPB_ENUM(GTPLatLng_FieldNumber) {
+ GTPLatLng_FieldNumber_Latitude = 1,
+ GTPLatLng_FieldNumber_Longitude = 2,
+};
+
+/**
+ * An object representing a latitude/longitude pair. This is expressed as a pair
+ * of doubles representing degrees latitude and degrees longitude. Unless
+ * specified otherwise, this must conform to the
+ * <a href="http://www.unoosa.org/pdf/icg/2012/template/WGS_84.pdf">WGS84
+ * standard</a>. Values must be within normalized ranges.
+ *
+ * Example of normalization code in Python:
+ *
+ * def NormalizeLongitude(longitude):
+ * """Wraps decimal degrees longitude to [-180.0, 180.0]."""
+ * q, r = divmod(longitude, 360.0)
+ * if r > 180.0 or (r == 180.0 and q <= -1.0):
+ * return r - 360.0
+ * return r
+ *
+ * def NormalizeLatLng(latitude, longitude):
+ * """Wraps decimal degrees latitude and longitude to
+ * [-90.0, 90.0] and [-180.0, 180.0], respectively."""
+ * r = latitude % 360.0
+ * if r <= 90.0:
+ * return r, NormalizeLongitude(longitude)
+ * elif r >= 270.0:
+ * return r - 360, NormalizeLongitude(longitude)
+ * else:
+ * return 180 - r, NormalizeLongitude(longitude + 180.0)
+ *
+ * assert 180.0 == NormalizeLongitude(180.0)
+ * assert -180.0 == NormalizeLongitude(-180.0)
+ * assert -179.0 == NormalizeLongitude(181.0)
+ * assert (0.0, 0.0) == NormalizeLatLng(360.0, 0.0)
+ * assert (0.0, 0.0) == NormalizeLatLng(-360.0, 0.0)
+ * assert (85.0, 180.0) == NormalizeLatLng(95.0, 0.0)
+ * assert (-85.0, -170.0) == NormalizeLatLng(-95.0, 10.0)
+ * assert (90.0, 10.0) == NormalizeLatLng(90.0, 10.0)
+ * assert (-90.0, -10.0) == NormalizeLatLng(-90.0, -10.0)
+ * assert (0.0, -170.0) == NormalizeLatLng(-180.0, 10.0)
+ * assert (0.0, -170.0) == NormalizeLatLng(180.0, 10.0)
+ * assert (-90.0, 10.0) == NormalizeLatLng(270.0, 10.0)
+ * assert (90.0, 10.0) == NormalizeLatLng(-270.0, 10.0)
+ **/
+@interface GTPLatLng : GPBMessage
+
+/** The latitude in degrees. It must be in the range [-90.0, +90.0]. */
+@property(nonatomic, readwrite) double latitude;
+
+/** The longitude in degrees. It must be in the range [-180.0, +180.0]. */
+@property(nonatomic, readwrite) double longitude;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+CF_EXTERN_C_END
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/objc/google/type/Latlng.pbobjc.m b/Firestore/Protos/objc/google/type/Latlng.pbobjc.m
new file mode 100644
index 0000000..9bb37ab
--- /dev/null
+++ b/Firestore/Protos/objc/google/type/Latlng.pbobjc.m
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/type/latlng.proto
+
+// This CPP symbol can be defined to use imports that match up to the framework
+// imports needed when using CocoaPods.
+#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS)
+ #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0
+#endif
+
+#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS
+ #import <Protobuf/GPBProtocolBuffers_RuntimeSupport.h>
+#else
+ #import "GPBProtocolBuffers_RuntimeSupport.h"
+#endif
+
+ #import "Latlng.pbobjc.h"
+// @@protoc_insertion_point(imports)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+
+#pragma mark - GTPLatlngRoot
+
+@implementation GTPLatlngRoot
+
+// No extensions in the file and no imports, so no need to generate
+// +extensionRegistry.
+
+@end
+
+#pragma mark - GTPLatlngRoot_FileDescriptor
+
+static GPBFileDescriptor *GTPLatlngRoot_FileDescriptor(void) {
+ // This is called by +initialize so there is no need to worry
+ // about thread safety of the singleton.
+ static GPBFileDescriptor *descriptor = NULL;
+ if (!descriptor) {
+ GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
+ descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.type"
+ objcPrefix:@"GTP"
+ syntax:GPBFileSyntaxProto3];
+ }
+ return descriptor;
+}
+
+#pragma mark - GTPLatLng
+
+@implementation GTPLatLng
+
+@dynamic latitude;
+@dynamic longitude;
+
+typedef struct GTPLatLng__storage_ {
+ uint32_t _has_storage_[1];
+ double latitude;
+ double longitude;
+} GTPLatLng__storage_;
+
+// This method is threadsafe because it is initially called
+// in +initialize for each subclass.
++ (GPBDescriptor *)descriptor {
+ static GPBDescriptor *descriptor = nil;
+ if (!descriptor) {
+ static GPBMessageFieldDescription fields[] = {
+ {
+ .name = "latitude",
+ .dataTypeSpecific.className = NULL,
+ .number = GTPLatLng_FieldNumber_Latitude,
+ .hasIndex = 0,
+ .offset = (uint32_t)offsetof(GTPLatLng__storage_, latitude),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeDouble,
+ },
+ {
+ .name = "longitude",
+ .dataTypeSpecific.className = NULL,
+ .number = GTPLatLng_FieldNumber_Longitude,
+ .hasIndex = 1,
+ .offset = (uint32_t)offsetof(GTPLatLng__storage_, longitude),
+ .flags = GPBFieldOptional,
+ .dataType = GPBDataTypeDouble,
+ },
+ };
+ GPBDescriptor *localDescriptor =
+ [GPBDescriptor allocDescriptorForClass:[GTPLatLng class]
+ rootClass:[GTPLatlngRoot class]
+ file:GTPLatlngRoot_FileDescriptor()
+ fields:fields
+ fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription))
+ storageSize:sizeof(GTPLatLng__storage_)
+ flags:GPBDescriptorInitializationFlag_None];
+ NSAssert(descriptor == nil, @"Startup recursed!");
+ descriptor = localDescriptor;
+ }
+ return descriptor;
+}
+
+@end
+
+
+#pragma clang diagnostic pop
+
+// @@protoc_insertion_point(global_scope)
diff --git a/Firestore/Protos/protos/firestore/local/maybe_document.proto b/Firestore/Protos/protos/firestore/local/maybe_document.proto
new file mode 100644
index 0000000..67d2f68
--- /dev/null
+++ b/Firestore/Protos/protos/firestore/local/maybe_document.proto
@@ -0,0 +1,33 @@
+syntax = "proto3";
+
+package firestore.client;
+
+option java_multiple_files = true;
+option java_package = "com.google.firebase.firestore.proto";
+
+option objc_class_prefix = "FSTPB";
+
+import "google/firestore/v1beta1/document.proto";
+import "google/protobuf/timestamp.proto";
+
+// A message indicating that the document is known to not exist.
+message NoDocument {
+ // The name of the document that does not exist, in the standard format:
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`
+ string name = 1;
+
+ // The time at which we observed that it does not exist.
+ google.protobuf.Timestamp read_time = 2;
+}
+
+// Represents either an existing document or the explicitly known absence of a
+// document.
+message MaybeDocument {
+ oneof document_type {
+ // Used if the document is known to not exist.
+ NoDocument no_document = 1;
+
+ // The document (if it exists).
+ google.firestore.v1beta1.Document document = 2;
+ }
+}
diff --git a/Firestore/Protos/protos/firestore/local/mutation.proto b/Firestore/Protos/protos/firestore/local/mutation.proto
new file mode 100644
index 0000000..6087f87
--- /dev/null
+++ b/Firestore/Protos/protos/firestore/local/mutation.proto
@@ -0,0 +1,44 @@
+syntax = "proto3";
+
+import "google/firestore/v1beta1/write.proto";
+import "google/protobuf/timestamp.proto";
+
+package firestore.client;
+
+option java_multiple_files = true;
+option java_package = "com.google.firebase.firestore.proto";
+
+option objc_class_prefix = "FSTPB";
+
+// Each user gets a single queue of WriteBatches to apply to the server.
+// MutationQueue tracks the metadata about the queue.
+message MutationQueue {
+ // An identifier for the highest numbered batch that has been acknowledged by
+ // the server. All WriteBatches in this queue with batch_ids less than or
+ // equal to this value are considered to have been acknowledged by the
+ // server.
+ int32 last_acknowledged_batch_id = 1;
+
+ // A stream token that was previously sent by the server.
+ //
+ // See StreamingWriteRequest in datastore.proto for more details about usage.
+ //
+ // After sending this token, earlier tokens may not be used anymore so only a
+ // single stream token is retained.
+ bytes last_stream_token = 2;
+}
+
+// Message containing a batch of user-level writes intended to be sent to
+// the server in a single call. Each user-level batch gets a separate
+// WriteBatch with a new batch_id.
+message WriteBatch {
+ // An identifier for this batch, allocated by the mutation queue in a
+ // monotonically increasing manner.
+ int32 batch_id = 1;
+
+ // A list of writes to apply. All writes will be applied atomically.
+ repeated google.firestore.v1beta1.Write writes = 2;
+
+ // The local time at which the write batch was initiated.
+ google.protobuf.Timestamp local_write_time = 3;
+}
diff --git a/Firestore/Protos/protos/firestore/local/target.proto b/Firestore/Protos/protos/firestore/local/target.proto
new file mode 100644
index 0000000..7f34515
--- /dev/null
+++ b/Firestore/Protos/protos/firestore/local/target.proto
@@ -0,0 +1,90 @@
+syntax = "proto3";
+
+package firestore.client;
+
+option java_multiple_files = true;
+option java_package = "com.google.firebase.firestore.proto";
+
+option objc_class_prefix = "FSTPB";
+
+import "google/firestore/v1beta1/firestore.proto";
+import "google/protobuf/timestamp.proto";
+
+// A Target is a long-lived data structure representing a resumable listen on a
+// particular user query. While the query describes what to listen to, the
+// Target records data about when the results were last updated and enough
+// information to be able to resume listening later.
+message Target {
+ // An auto-generated sequential numeric identifier for the target. This
+ // serves as the identity of the target, and once assigned never changes.
+ int32 target_id = 1;
+
+ // The last snapshot version received from the Watch Service for this target.
+ //
+ // This is the same value as TargetChange.read_time
+ google.protobuf.Timestamp snapshot_version = 2;
+
+ // An opaque, server-assigned token that allows watching a query to be
+ // resumed after disconnecting without retransmitting all the data that
+ // matches the query. The resume token essentially identifies a point in
+ // time from which the server should resume sending results.
+ //
+ // This is related to the snapshot_version in that the resume_token
+ // effectively also encodes that value, but the resume_token is opaque and
+ // sometimes encodes additional information.
+ //
+ // A consequence of this is that the resume_token should be used when asking
+ // the server to reason about where this client is in the watch stream, but
+ // the client should use the snapshot_version for its own purposes.
+ //
+ // This is the same value as TargetChange.resume_token
+ bytes resume_token = 3;
+
+ // A sequence number representing the last time this query was listened to,
+ // used for garbage collection purposes.
+ //
+ // Conventionally this would be a timestamp value, but device-local clocks
+ // are unreliable and they must be able to create new listens even while
+ // disconnected. Instead this should be a monotonically increasing number
+ // that's incremented on each listen call.
+ //
+ // This is different from the target_id since the target_id is an immutable
+ // identifier assigned to the Target on first use while
+ // last_listen_sequence_number is updated every time the query is listened
+ // to.
+ int64 last_listen_sequence_number = 4;
+
+ // The server-side type of target to listen to.
+ oneof target_type {
+ // A target specified by a query.
+ google.firestore.v1beta1.Target.QueryTarget query = 5;
+
+ // A target specified by a set of document names.
+ google.firestore.v1beta1.Target.DocumentsTarget documents = 6;
+ }
+}
+
+// Global state tracked across all Targets, tracked separately to avoid the
+// need for extra indexes.
+message TargetGlobal {
+ // The highest numbered target id across all Targets.
+ //
+ // See Target.target_id.
+ int32 highest_target_id = 1;
+
+ // The highest numbered last_listen_sequence_number across all Targets.
+ //
+ // See Target.last_listen_sequence_number.
+ int64 highest_listen_sequence_number = 2;
+
+ // A global snapshot version representing the last consistent snapshot we
+ // received from the backend. This is monotonically increasing and any
+ // snapshots received from the backend prior to this version (e.g. for
+ // targets resumed with a resume_token) should be suppressed (buffered) until
+ // the backend has caught up to this snapshot_version again. This prevents
+ // our cache from ever going backwards in time.
+ //
+ // This is updated whenever our we get a TargetChange with a read_time and
+ // empty target_ids.
+ google.protobuf.Timestamp last_remote_snapshot_version = 3;
+}
diff --git a/Firestore/Protos/protos/google/api/annotations.proto b/Firestore/Protos/protos/google/api/annotations.proto
new file mode 100644
index 0000000..85c361b
--- /dev/null
+++ b/Firestore/Protos/protos/google/api/annotations.proto
@@ -0,0 +1,31 @@
+// Copyright (c) 2015, Google 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.
+
+syntax = "proto3";
+
+package google.api;
+
+import "google/api/http.proto";
+import "google/protobuf/descriptor.proto";
+
+option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
+option java_multiple_files = true;
+option java_outer_classname = "AnnotationsProto";
+option java_package = "com.google.api";
+option objc_class_prefix = "GAPI";
+
+extend google.protobuf.MethodOptions {
+ // See `HttpRule`.
+ HttpRule http = 72295728;
+}
diff --git a/Firestore/Protos/protos/google/api/http.proto b/Firestore/Protos/protos/google/api/http.proto
new file mode 100644
index 0000000..5f8538a
--- /dev/null
+++ b/Firestore/Protos/protos/google/api/http.proto
@@ -0,0 +1,291 @@
+// Copyright 2016 Google 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.
+
+syntax = "proto3";
+
+package google.api;
+
+option cc_enable_arenas = true;
+option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
+option java_multiple_files = true;
+option java_outer_classname = "HttpProto";
+option java_package = "com.google.api";
+option objc_class_prefix = "GAPI";
+
+
+// Defines the HTTP configuration for a service. It contains a list of
+// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
+// to one or more HTTP REST API methods.
+message Http {
+ // A list of HTTP configuration rules that apply to individual API methods.
+ //
+ // **NOTE:** All service configuration rules follow "last one wins" order.
+ repeated HttpRule rules = 1;
+}
+
+// `HttpRule` defines the mapping of an RPC method to one or more HTTP
+// REST APIs. The mapping determines what portions of the request
+// message are populated from the path, query parameters, or body of
+// the HTTP request. The mapping is typically specified as an
+// `google.api.http` annotation, see "google/api/annotations.proto"
+// for details.
+//
+// The mapping consists of a field specifying the path template and
+// method kind. The path template can refer to fields in the request
+// message, as in the example below which describes a REST GET
+// operation on a resource collection of messages:
+//
+//
+// service Messaging {
+// rpc GetMessage(GetMessageRequest) returns (Message) {
+// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}";
+// }
+// }
+// message GetMessageRequest {
+// message SubMessage {
+// string subfield = 1;
+// }
+// string message_id = 1; // mapped to the URL
+// SubMessage sub = 2; // `sub.subfield` is url-mapped
+// }
+// message Message {
+// string text = 1; // content of the resource
+// }
+//
+// The same http annotation can alternatively be expressed inside the
+// `GRPC API Configuration` YAML file.
+//
+// http:
+// rules:
+// - selector: <proto_package_name>.Messaging.GetMessage
+// get: /v1/messages/{message_id}/{sub.subfield}
+//
+// This definition enables an automatic, bidrectional mapping of HTTP
+// JSON to RPC. Example:
+//
+// HTTP | RPC
+// -----|-----
+// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))`
+//
+// In general, not only fields but also field paths can be referenced
+// from a path pattern. Fields mapped to the path pattern cannot be
+// repeated and must have a primitive (non-message) type.
+//
+// Any fields in the request message which are not bound by the path
+// pattern automatically become (optional) HTTP query
+// parameters. Assume the following definition of the request message:
+//
+//
+// message GetMessageRequest {
+// message SubMessage {
+// string subfield = 1;
+// }
+// string message_id = 1; // mapped to the URL
+// int64 revision = 2; // becomes a parameter
+// SubMessage sub = 3; // `sub.subfield` becomes a parameter
+// }
+//
+//
+// This enables a HTTP JSON to RPC mapping as below:
+//
+// HTTP | RPC
+// -----|-----
+// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))`
+//
+// Note that fields which are mapped to HTTP parameters must have a
+// primitive type or a repeated primitive type. Message types are not
+// allowed. In the case of a repeated type, the parameter can be
+// repeated in the URL, as in `...?param=A&param=B`.
+//
+// For HTTP method kinds which allow a request body, the `body` field
+// specifies the mapping. Consider a REST update method on the
+// message resource collection:
+//
+//
+// service Messaging {
+// rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
+// option (google.api.http) = {
+// put: "/v1/messages/{message_id}"
+// body: "message"
+// };
+// }
+// }
+// message UpdateMessageRequest {
+// string message_id = 1; // mapped to the URL
+// Message message = 2; // mapped to the body
+// }
+//
+//
+// The following HTTP JSON to RPC mapping is enabled, where the
+// representation of the JSON in the request body is determined by
+// protos JSON encoding:
+//
+// HTTP | RPC
+// -----|-----
+// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })`
+//
+// The special name `*` can be used in the body mapping to define that
+// every field not bound by the path template should be mapped to the
+// request body. This enables the following alternative definition of
+// the update method:
+//
+// service Messaging {
+// rpc UpdateMessage(Message) returns (Message) {
+// option (google.api.http) = {
+// put: "/v1/messages/{message_id}"
+// body: "*"
+// };
+// }
+// }
+// message Message {
+// string message_id = 1;
+// string text = 2;
+// }
+//
+//
+// The following HTTP JSON to RPC mapping is enabled:
+//
+// HTTP | RPC
+// -----|-----
+// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")`
+//
+// Note that when using `*` in the body mapping, it is not possible to
+// have HTTP parameters, as all fields not bound by the path end in
+// the body. This makes this option more rarely used in practice of
+// defining REST APIs. The common usage of `*` is in custom methods
+// which don't use the URL at all for transferring data.
+//
+// It is possible to define multiple HTTP methods for one RPC by using
+// the `additional_bindings` option. Example:
+//
+// service Messaging {
+// rpc GetMessage(GetMessageRequest) returns (Message) {
+// option (google.api.http) = {
+// get: "/v1/messages/{message_id}"
+// additional_bindings {
+// get: "/v1/users/{user_id}/messages/{message_id}"
+// }
+// };
+// }
+// }
+// message GetMessageRequest {
+// string message_id = 1;
+// string user_id = 2;
+// }
+//
+//
+// This enables the following two alternative HTTP JSON to RPC
+// mappings:
+//
+// HTTP | RPC
+// -----|-----
+// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
+// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")`
+//
+// # Rules for HTTP mapping
+//
+// The rules for mapping HTTP path, query parameters, and body fields
+// to the request message are as follows:
+//
+// 1. The `body` field specifies either `*` or a field path, or is
+// omitted. If omitted, it assumes there is no HTTP body.
+// 2. Leaf fields (recursive expansion of nested messages in the
+// request) can be classified into three types:
+// (a) Matched in the URL template.
+// (b) Covered by body (if body is `*`, everything except (a) fields;
+// else everything under the body field)
+// (c) All other fields.
+// 3. URL query parameters found in the HTTP request are mapped to (c) fields.
+// 4. Any body sent with an HTTP request can contain only (b) fields.
+//
+// The syntax of the path template is as follows:
+//
+// Template = "/" Segments [ Verb ] ;
+// Segments = Segment { "/" Segment } ;
+// Segment = "*" | "**" | LITERAL | Variable ;
+// Variable = "{" FieldPath [ "=" Segments ] "}" ;
+// FieldPath = IDENT { "." IDENT } ;
+// Verb = ":" LITERAL ;
+//
+// The syntax `*` matches a single path segment. It follows the semantics of
+// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String
+// Expansion.
+//
+// The syntax `**` matches zero or more path segments. It follows the semantics
+// of [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.3 Reserved
+// Expansion. NOTE: it must be the last segment in the path except the Verb.
+//
+// The syntax `LITERAL` matches literal text in the URL path.
+//
+// The syntax `Variable` matches the entire path as specified by its template;
+// this nested template must not contain further variables. If a variable
+// matches a single path segment, its template may be omitted, e.g. `{var}`
+// is equivalent to `{var=*}`.
+//
+// NOTE: the field paths in variables and in the `body` must not refer to
+// repeated fields or map fields.
+//
+// Use CustomHttpPattern to specify any HTTP method that is not included in the
+// `pattern` field, such as HEAD, or "*" to leave the HTTP method unspecified for
+// a given URL path rule. The wild-card rule is useful for services that provide
+// content to Web (HTML) clients.
+message HttpRule {
+ // Selects methods to which this rule applies.
+ //
+ // Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
+ string selector = 1;
+
+ // Determines the URL pattern is matched by this rules. This pattern can be
+ // used with any of the {get|put|post|delete|patch} methods. A custom method
+ // can be defined using the 'custom' field.
+ oneof pattern {
+ // Used for listing and getting information about resources.
+ string get = 2;
+
+ // Used for updating a resource.
+ string put = 3;
+
+ // Used for creating a resource.
+ string post = 4;
+
+ // Used for deleting a resource.
+ string delete = 5;
+
+ // Used for updating a resource.
+ string patch = 6;
+
+ // Custom pattern is used for defining custom verbs.
+ CustomHttpPattern custom = 8;
+ }
+
+ // The name of the request field whose value is mapped to the HTTP body, or
+ // `*` for mapping all fields not captured by the path pattern to the HTTP
+ // body. NOTE: the referred field must not be a repeated field and must be
+ // present at the top-level of request message type.
+ string body = 7;
+
+ // Additional HTTP bindings for the selector. Nested bindings must
+ // not contain an `additional_bindings` field themselves (that is,
+ // the nesting may only be one level deep).
+ repeated HttpRule additional_bindings = 11;
+}
+
+// A custom pattern is used for defining custom HTTP verb.
+message CustomHttpPattern {
+ // The name of this custom HTTP verb.
+ string kind = 1;
+
+ // The path matched by this custom verb.
+ string path = 2;
+}
diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/common.proto b/Firestore/Protos/protos/google/firestore/v1beta1/common.proto
new file mode 100644
index 0000000..e624323
--- /dev/null
+++ b/Firestore/Protos/protos/google/firestore/v1beta1/common.proto
@@ -0,0 +1,82 @@
+// Copyright 2017 Google 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.
+
+syntax = "proto3";
+
+package google.firestore.v1beta1;
+
+import "google/api/annotations.proto";
+import "google/protobuf/timestamp.proto";
+
+option csharp_namespace = "Google.Cloud.Firestore.V1Beta1";
+option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore";
+option java_multiple_files = true;
+option java_outer_classname = "CommonProto";
+option java_package = "com.google.firestore.v1beta1";
+option objc_class_prefix = "GCFS";
+
+
+// A set of field paths on a document.
+// Used to restrict a get or update operation on a document to a subset of its
+// fields.
+// This is different from standard field masks, as this is always scoped to a
+// [Document][google.firestore.v1beta1.Document], and takes in account the dynamic nature of [Value][google.firestore.v1beta1.Value].
+message DocumentMask {
+ // The list of field paths in the mask. See [Document.fields][google.firestore.v1beta1.Document.fields] for a field
+ // path syntax reference.
+ repeated string field_paths = 1;
+}
+
+// A precondition on a document, used for conditional operations.
+message Precondition {
+ // The type of precondition.
+ oneof condition_type {
+ // When set to `true`, the target document must exist.
+ // When set to `false`, the target document must not exist.
+ bool exists = 1;
+
+ // When set, the target document must exist and have been last updated at
+ // that time.
+ google.protobuf.Timestamp update_time = 2;
+ }
+}
+
+// Options for creating a new transaction.
+message TransactionOptions {
+ // Options for a transaction that can be used to read and write documents.
+ message ReadWrite {
+ // An optional transaction to retry.
+ bytes retry_transaction = 1;
+ }
+
+ // Options for a transaction that can only be used to read documents.
+ message ReadOnly {
+ // The consistency mode for this transaction. If not set, defaults to strong
+ // consistency.
+ oneof consistency_selector {
+ // Reads documents at the given time.
+ // This may not be older than 60 seconds.
+ google.protobuf.Timestamp read_time = 2;
+ }
+ }
+
+ // The mode of the transaction.
+ oneof mode {
+ // The transaction can only be used for read operations.
+ ReadOnly read_only = 2;
+
+ // The transaction can be used for both read and write operations.
+ ReadWrite read_write = 3;
+ }
+}
diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/document.proto b/Firestore/Protos/protos/google/firestore/v1beta1/document.proto
new file mode 100644
index 0000000..cf6001d
--- /dev/null
+++ b/Firestore/Protos/protos/google/firestore/v1beta1/document.proto
@@ -0,0 +1,148 @@
+// Copyright 2017 Google 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.
+
+syntax = "proto3";
+
+package google.firestore.v1beta1;
+
+import "google/api/annotations.proto";
+import "google/protobuf/struct.proto";
+import "google/protobuf/timestamp.proto";
+import "google/type/latlng.proto";
+
+option csharp_namespace = "Google.Cloud.Firestore.V1Beta1";
+option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore";
+option java_multiple_files = true;
+option java_outer_classname = "DocumentProto";
+option java_package = "com.google.firestore.v1beta1";
+option objc_class_prefix = "GCFS";
+
+
+// A Firestore document.
+//
+// Must not exceed 1 MiB - 4 bytes.
+message Document {
+ // The resource name of the document, for example
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ string name = 1;
+
+ // The document's fields.
+ //
+ // The map keys represent field names.
+ //
+ // A simple field name contains only characters `a` to `z`, `A` to `Z`,
+ // `0` to `9`, or `_`, and must not start with `0` to `9` or `_`. For example,
+ // `foo_bar_17`.
+ //
+ // Field names matching the regular expression `__.*__` are reserved. Reserved
+ // field names are forbidden except in certain documented contexts. The map
+ // keys, represented as UTF-8, must not exceed 1,500 bytes and cannot be
+ // empty.
+ //
+ // Field paths may be used in other contexts to refer to structured fields
+ // defined here. For `map_value`, the field path is represented by the simple
+ // or quoted field names of the containing fields, delimited by `.`. For
+ // example, the structured field
+ // `"foo" : { map_value: { "x&y" : { string_value: "hello" }}}` would be
+ // represented by the field path `foo.x&y`.
+ //
+ // Within a field path, a quoted field name starts and ends with `` ` `` and
+ // may contain any character. Some characters, including `` ` ``, must be
+ // escaped using a `\`. For example, `` `x&y` `` represents `x&y` and
+ // `` `bak\`tik` `` represents `` bak`tik ``.
+ map<string, Value> fields = 2;
+
+ // Output only. The time at which the document was created.
+ //
+ // This value increases monotonically when a document is deleted then
+ // recreated. It can also be compared to values from other documents and
+ // the `read_time` of a query.
+ google.protobuf.Timestamp create_time = 3;
+
+ // Output only. The time at which the document was last changed.
+ //
+ // This value is initally set to the `create_time` then increases
+ // monotonically with each change to the document. It can also be
+ // compared to values from other documents and the `read_time` of a query.
+ google.protobuf.Timestamp update_time = 4;
+}
+
+// A message that can hold any of the supported value types.
+message Value {
+ // Must have a value set.
+ oneof value_type {
+ // A null value.
+ google.protobuf.NullValue null_value = 11;
+
+ // A boolean value.
+ bool boolean_value = 1;
+
+ // An integer value.
+ int64 integer_value = 2;
+
+ // A double value.
+ double double_value = 3;
+
+ // A timestamp value.
+ //
+ // Precise only to microseconds. When stored, any additional precision is
+ // rounded down.
+ google.protobuf.Timestamp timestamp_value = 10;
+
+ // A string value.
+ //
+ // The string, represented as UTF-8, must not exceed 1 MiB - 89 bytes.
+ // Only the first 1,500 bytes of the UTF-8 representation are considered by
+ // queries.
+ string string_value = 17;
+
+ // A bytes value.
+ //
+ // Must not exceed 1 MiB - 89 bytes.
+ // Only the first 1,500 bytes are considered by queries.
+ bytes bytes_value = 18;
+
+ // A reference to a document. For example:
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ string reference_value = 5;
+
+ // A geo point value representing a point on the surface of Earth.
+ google.type.LatLng geo_point_value = 8;
+
+ // An array value.
+ //
+ // Cannot contain another array value.
+ ArrayValue array_value = 9;
+
+ // A map value.
+ MapValue map_value = 6;
+ }
+}
+
+// An array value.
+message ArrayValue {
+ // Values in the array.
+ repeated Value values = 1;
+}
+
+// A map value.
+message MapValue {
+ // The map's fields.
+ //
+ // The map keys represent field names. Field names matching the regular
+ // expression `__.*__` are reserved. Reserved field names are forbidden except
+ // in certain documented contexts. The map keys, represented as UTF-8, must
+ // not exceed 1,500 bytes and cannot be empty.
+ map<string, Value> fields = 1;
+}
diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/firestore.proto b/Firestore/Protos/protos/google/firestore/v1beta1/firestore.proto
new file mode 100644
index 0000000..3939caa
--- /dev/null
+++ b/Firestore/Protos/protos/google/firestore/v1beta1/firestore.proto
@@ -0,0 +1,719 @@
+// Copyright 2017 Google 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.
+
+syntax = "proto3";
+
+package google.firestore.v1beta1;
+
+import "google/api/annotations.proto";
+import "google/firestore/v1beta1/common.proto";
+import "google/firestore/v1beta1/document.proto";
+import "google/firestore/v1beta1/query.proto";
+import "google/firestore/v1beta1/write.proto";
+import "google/protobuf/empty.proto";
+import "google/protobuf/timestamp.proto";
+import "google/rpc/status.proto";
+
+option csharp_namespace = "Google.Cloud.Firestore.V1Beta1";
+option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore";
+option java_multiple_files = true;
+option java_outer_classname = "FirestoreProto";
+option java_package = "com.google.firestore.v1beta1";
+option objc_class_prefix = "GCFS";
+
+// Specification of the Firestore API.
+
+
+
+// The Cloud Firestore service.
+//
+// This service exposes several types of comparable timestamps:
+//
+// * `create_time` - The time at which a document was created. Changes only
+// when a document is deleted, then re-created. Increases in a strict
+// monotonic fashion.
+// * `update_time` - The time at which a document was last updated. Changes
+// every time a document is modified. Does not change when a write results
+// in no modifications. Increases in a strict monotonic fashion.
+// * `read_time` - The time at which a particular state was observed. Used
+// to denote a consistent snapshot of the database or the time at which a
+// Document was observed to not exist.
+// * `commit_time` - The time at which the writes in a transaction were
+// committed. Any read with an equal or greater `read_time` is guaranteed
+// to see the effects of the transaction.
+service Firestore {
+ // Gets a single document.
+ rpc GetDocument(GetDocumentRequest) returns (Document) {
+ option (google.api.http) = { get: "/v1beta1/{name=projects/*/databases/*/documents/*/**}" };
+ }
+
+ // Lists documents.
+ rpc ListDocuments(ListDocumentsRequest) returns (ListDocumentsResponse) {
+ option (google.api.http) = { get: "/v1beta1/{parent=projects/*/databases/*/documents/*/**}/{collection_id}" };
+ }
+
+ // Creates a new document.
+ rpc CreateDocument(CreateDocumentRequest) returns (Document) {
+ option (google.api.http) = { post: "/v1beta1/{parent=projects/*/databases/*/documents/**}/{collection_id}" body: "document" };
+ }
+
+ // Updates or inserts a document.
+ rpc UpdateDocument(UpdateDocumentRequest) returns (Document) {
+ option (google.api.http) = { patch: "/v1beta1/{document.name=projects/*/databases/*/documents/*/**}" body: "document" };
+ }
+
+ // Deletes a document.
+ rpc DeleteDocument(DeleteDocumentRequest) returns (google.protobuf.Empty) {
+ option (google.api.http) = { delete: "/v1beta1/{name=projects/*/databases/*/documents/*/**}" };
+ }
+
+ // Gets multiple documents.
+ //
+ // Documents returned by this method are not guaranteed to be returned in the
+ // same order that they were requested.
+ rpc BatchGetDocuments(BatchGetDocumentsRequest) returns (stream BatchGetDocumentsResponse) {
+ option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:batchGet" body: "*" };
+ }
+
+ // Starts a new transaction.
+ rpc BeginTransaction(BeginTransactionRequest) returns (BeginTransactionResponse) {
+ option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:beginTransaction" body: "*" };
+ }
+
+ // Commits a transaction, while optionally updating documents.
+ rpc Commit(CommitRequest) returns (CommitResponse) {
+ option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:commit" body: "*" };
+ }
+
+ // Rolls back a transaction.
+ rpc Rollback(RollbackRequest) returns (google.protobuf.Empty) {
+ option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:rollback" body: "*" };
+ }
+
+ // Runs a query.
+ rpc RunQuery(RunQueryRequest) returns (stream RunQueryResponse) {
+ option (google.api.http) = { post: "/v1beta1/{parent=projects/*/databases/*/documents}:runQuery" body: "*" };
+ }
+
+ // Streams batches of document updates and deletes, in order.
+ rpc Write(stream WriteRequest) returns (stream WriteResponse) {
+ option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:write" body: "*" };
+ }
+
+ // Listens to changes.
+ rpc Listen(stream ListenRequest) returns (stream ListenResponse) {
+ option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:listen" body: "*" };
+ }
+
+ // Lists all the collection IDs underneath a document.
+ rpc ListCollectionIds(ListCollectionIdsRequest) returns (ListCollectionIdsResponse) {
+ option (google.api.http) = { post: "/v1beta1/{parent=projects/*/databases/*/documents}:listCollectionIds" body: "*" };
+ }
+}
+
+// The request for [Firestore.GetDocument][google.firestore.v1beta1.Firestore.GetDocument].
+message GetDocumentRequest {
+ // The resource name of the Document to get. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ string name = 1;
+
+ // The fields to return. If not set, returns all fields.
+ //
+ // If the document has a field that is not present in this mask, that field
+ // will not be returned in the response.
+ DocumentMask mask = 2;
+
+ // The consistency mode for this transaction.
+ // If not set, defaults to strong consistency.
+ oneof consistency_selector {
+ // Reads the document in a transaction.
+ bytes transaction = 3;
+
+ // Reads the version of the document at the given time.
+ // This may not be older than 60 seconds.
+ google.protobuf.Timestamp read_time = 5;
+ }
+}
+
+// The request for [Firestore.ListDocuments][google.firestore.v1beta1.Firestore.ListDocuments].
+message ListDocumentsRequest {
+ // The parent resource name. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents` or
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ // For example:
+ // `projects/my-project/databases/my-database/documents` or
+ // `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom`
+ string parent = 1;
+
+ // The collection ID, relative to `parent`, to list. For example: `chatrooms`
+ // or `messages`.
+ string collection_id = 2;
+
+ // The maximum number of documents to return.
+ int32 page_size = 3;
+
+ // The `next_page_token` value returned from a previous List request, if any.
+ string page_token = 4;
+
+ // The order to sort results by. For example: `priority desc, name`.
+ string order_by = 6;
+
+ // The fields to return. If not set, returns all fields.
+ //
+ // If a document has a field that is not present in this mask, that field
+ // will not be returned in the response.
+ DocumentMask mask = 7;
+
+ // The consistency mode for this transaction.
+ // If not set, defaults to strong consistency.
+ oneof consistency_selector {
+ // Reads documents in a transaction.
+ bytes transaction = 8;
+
+ // Reads documents as they were at the given time.
+ // This may not be older than 60 seconds.
+ google.protobuf.Timestamp read_time = 10;
+ }
+
+ // If the list should show missing documents. A missing document is a
+ // document that does not exist but has sub-documents. These documents will
+ // be returned with a key but will not have fields, [Document.create_time][google.firestore.v1beta1.Document.create_time],
+ // or [Document.update_time][google.firestore.v1beta1.Document.update_time] set.
+ //
+ // Requests with `show_missing` may not specify `where` or
+ // `order_by`.
+ bool show_missing = 12;
+}
+
+// The response for [Firestore.ListDocuments][google.firestore.v1beta1.Firestore.ListDocuments].
+message ListDocumentsResponse {
+ // The Documents found.
+ repeated Document documents = 1;
+
+ // The next page token.
+ string next_page_token = 2;
+}
+
+// The request for [Firestore.CreateDocument][google.firestore.v1beta1.Firestore.CreateDocument].
+message CreateDocumentRequest {
+ // The parent resource. For example:
+ // `projects/{project_id}/databases/{database_id}/documents` or
+ // `projects/{project_id}/databases/{database_id}/documents/chatrooms/{chatroom_id}`
+ string parent = 1;
+
+ // The collection ID, relative to `parent`, to list. For example: `chatrooms`.
+ string collection_id = 2;
+
+ // The client-assigned document ID to use for this document.
+ //
+ // Optional. If not specified, an ID will be assigned by the service.
+ string document_id = 3;
+
+ // The document to create. `name` must not be set.
+ Document document = 4;
+
+ // The fields to return. If not set, returns all fields.
+ //
+ // If the document has a field that is not present in this mask, that field
+ // will not be returned in the response.
+ DocumentMask mask = 5;
+}
+
+// The request for [Firestore.UpdateDocument][google.firestore.v1beta1.Firestore.UpdateDocument].
+message UpdateDocumentRequest {
+ // The updated document.
+ // Creates the document if it does not already exist.
+ Document document = 1;
+
+ // The fields to update.
+ // None of the field paths in the mask may contain a reserved name.
+ //
+ // If the document exists on the server and has fields not referenced in the
+ // mask, they are left unchanged.
+ // Fields referenced in the mask, but not present in the input document, are
+ // deleted from the document on the server.
+ DocumentMask update_mask = 2;
+
+ // The fields to return. If not set, returns all fields.
+ //
+ // If the document has a field that is not present in this mask, that field
+ // will not be returned in the response.
+ DocumentMask mask = 3;
+
+ // An optional precondition on the document.
+ // The request will fail if this is set and not met by the target document.
+ Precondition current_document = 4;
+}
+
+// The request for [Firestore.DeleteDocument][google.firestore.v1beta1.Firestore.DeleteDocument].
+message DeleteDocumentRequest {
+ // The resource name of the Document to delete. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ string name = 1;
+
+ // An optional precondition on the document.
+ // The request will fail if this is set and not met by the target document.
+ Precondition current_document = 2;
+}
+
+// The request for [Firestore.BatchGetDocuments][google.firestore.v1beta1.Firestore.BatchGetDocuments].
+message BatchGetDocumentsRequest {
+ // The database name. In the format:
+ // `projects/{project_id}/databases/{database_id}`.
+ string database = 1;
+
+ // The names of the documents to retrieve. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ // The request will fail if any of the document is not a child resource of the
+ // given `database`. Duplicate names will be elided.
+ repeated string documents = 2;
+
+ // The fields to return. If not set, returns all fields.
+ //
+ // If a document has a field that is not present in this mask, that field will
+ // not be returned in the response.
+ DocumentMask mask = 3;
+
+ // The consistency mode for this transaction.
+ // If not set, defaults to strong consistency.
+ oneof consistency_selector {
+ // Reads documents in a transaction.
+ bytes transaction = 4;
+
+ // Starts a new transaction and reads the documents.
+ // Defaults to a read-only transaction.
+ // The new transaction ID will be returned as the first response in the
+ // stream.
+ TransactionOptions new_transaction = 5;
+
+ // Reads documents as they were at the given time.
+ // This may not be older than 60 seconds.
+ google.protobuf.Timestamp read_time = 7;
+ }
+}
+
+// The streamed response for [Firestore.BatchGetDocuments][google.firestore.v1beta1.Firestore.BatchGetDocuments].
+message BatchGetDocumentsResponse {
+ // A single result.
+ // This can be empty if the server is just returning a transaction.
+ oneof result {
+ // A document that was requested.
+ Document found = 1;
+
+ // A document name that was requested but does not exist. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ string missing = 2;
+ }
+
+ // The transaction that was started as part of this request.
+ // Will only be set in the first response, and only if
+ // [BatchGetDocumentsRequest.new_transaction][google.firestore.v1beta1.BatchGetDocumentsRequest.new_transaction] was set in the request.
+ bytes transaction = 3;
+
+ // The time at which the document was read.
+ // This may be monotically increasing, in this case the previous documents in
+ // the result stream are guaranteed not to have changed between their
+ // read_time and this one.
+ google.protobuf.Timestamp read_time = 4;
+}
+
+// The request for [Firestore.BeginTransaction][google.firestore.v1beta1.Firestore.BeginTransaction].
+message BeginTransactionRequest {
+ // The database name. In the format:
+ // `projects/{project_id}/databases/{database_id}`.
+ string database = 1;
+
+ // The options for the transaction.
+ // Defaults to a read-write transaction.
+ TransactionOptions options = 2;
+}
+
+// The response for [Firestore.BeginTransaction][google.firestore.v1beta1.Firestore.BeginTransaction].
+message BeginTransactionResponse {
+ // The transaction that was started.
+ bytes transaction = 1;
+}
+
+// The request for [Firestore.Commit][google.firestore.v1beta1.Firestore.Commit].
+message CommitRequest {
+ // The database name. In the format:
+ // `projects/{project_id}/databases/{database_id}`.
+ string database = 1;
+
+ // The writes to apply.
+ //
+ // Always executed atomically and in order.
+ repeated Write writes = 2;
+
+ // If non-empty, applies all writes in this transaction, and commits it.
+ // Otherwise, applies the writes as if they were in their own transaction.
+ bytes transaction = 3;
+}
+
+// The response for [Firestore.Commit][google.firestore.v1beta1.Firestore.Commit].
+message CommitResponse {
+ // The result of applying the writes.
+ //
+ // This i-th write result corresponds to the i-th write in the
+ // request.
+ repeated WriteResult write_results = 1;
+
+ // The time at which the commit occurred.
+ google.protobuf.Timestamp commit_time = 2;
+}
+
+// The request for [Firestore.Rollback][google.firestore.v1beta1.Firestore.Rollback].
+message RollbackRequest {
+ // The database name. In the format:
+ // `projects/{project_id}/databases/{database_id}`.
+ string database = 1;
+
+ // The transaction to roll back.
+ bytes transaction = 2;
+}
+
+// The request for [Firestore.RunQuery][google.firestore.v1beta1.Firestore.RunQuery].
+message RunQueryRequest {
+ // The parent resource name. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents` or
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ // For example:
+ // `projects/my-project/databases/my-database/documents` or
+ // `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom`
+ string parent = 1;
+
+ // The query to run.
+ oneof query_type {
+ // A structured query.
+ StructuredQuery structured_query = 2;
+ }
+
+ // The consistency mode for this transaction.
+ // If not set, defaults to strong consistency.
+ oneof consistency_selector {
+ // Reads documents in a transaction.
+ bytes transaction = 5;
+
+ // Starts a new transaction and reads the documents.
+ // Defaults to a read-only transaction.
+ // The new transaction ID will be returned as the first response in the
+ // stream.
+ TransactionOptions new_transaction = 6;
+
+ // Reads documents as they were at the given time.
+ // This may not be older than 60 seconds.
+ google.protobuf.Timestamp read_time = 7;
+ }
+}
+
+// The response for [Firestore.RunQuery][google.firestore.v1beta1.Firestore.RunQuery].
+message RunQueryResponse {
+ // The transaction that was started as part of this request.
+ // Can only be set in the first response, and only if
+ // [RunQueryRequest.new_transaction][google.firestore.v1beta1.RunQueryRequest.new_transaction] was set in the request.
+ // If set, no other fields will be set in this response.
+ bytes transaction = 2;
+
+ // A query result.
+ // Not set when reporting partial progress.
+ Document document = 1;
+
+ // The time at which the document was read. This may be monotonically
+ // increasing; in this case, the previous documents in the result stream are
+ // guaranteed not to have changed between their `read_time` and this one.
+ //
+ // If the query returns no results, a response with `read_time` and no
+ // `document` will be sent, and this represents the time at which the query
+ // was run.
+ google.protobuf.Timestamp read_time = 3;
+
+ // The number of results that have been skipped due to an offset between
+ // the last response and the current response.
+ int32 skipped_results = 4;
+}
+
+// The request for [Firestore.Write][google.firestore.v1beta1.Firestore.Write].
+//
+// The first request creates a stream, or resumes an existing one from a token.
+//
+// When creating a new stream, the server replies with a response containing
+// only an ID and a token, to use in the next request.
+//
+// When resuming a stream, the server first streams any responses later than the
+// given token, then a response containing only an up-to-date token, to use in
+// the next request.
+message WriteRequest {
+ // The database name. In the format:
+ // `projects/{project_id}/databases/{database_id}`.
+ // This is only required in the first message.
+ string database = 1;
+
+ // The ID of the write stream to resume.
+ // This may only be set in the first message. When left empty, a new write
+ // stream will be created.
+ string stream_id = 2;
+
+ // The writes to apply.
+ //
+ // Always executed atomically and in order.
+ // This must be empty on the first request.
+ // This may be empty on the last request.
+ // This must not be empty on all other requests.
+ repeated Write writes = 3;
+
+ // A stream token that was previously sent by the server.
+ //
+ // The client should set this field to the token from the most recent
+ // [WriteResponse][google.firestore.v1beta1.WriteResponse] it has received. This acknowledges that the client has
+ // received responses up to this token. After sending this token, earlier
+ // tokens may not be used anymore.
+ //
+ // The server may close the stream if there are too many unacknowledged
+ // responses.
+ //
+ // Leave this field unset when creating a new stream. To resume a stream at
+ // a specific point, set this field and the `stream_id` field.
+ //
+ // Leave this field unset when creating a new stream.
+ bytes stream_token = 4;
+
+ // Labels associated with this write request.
+ map<string, string> labels = 5;
+}
+
+// The response for [Firestore.Write][google.firestore.v1beta1.Firestore.Write].
+message WriteResponse {
+ // The ID of the stream.
+ // Only set on the first message, when a new stream was created.
+ string stream_id = 1;
+
+ // A token that represents the position of this response in the stream.
+ // This can be used by a client to resume the stream at this point.
+ //
+ // This field is always set.
+ bytes stream_token = 2;
+
+ // The result of applying the writes.
+ //
+ // This i-th write result corresponds to the i-th write in the
+ // request.
+ repeated WriteResult write_results = 3;
+
+ // The time at which the commit occurred.
+ google.protobuf.Timestamp commit_time = 4;
+}
+
+// A request for [Firestore.Listen][google.firestore.v1beta1.Firestore.Listen]
+message ListenRequest {
+ // The database name. In the format:
+ // `projects/{project_id}/databases/{database_id}`.
+ string database = 1;
+
+ // The supported target changes.
+ oneof target_change {
+ // A target to add to this stream.
+ Target add_target = 2;
+
+ // The ID of a target to remove from this stream.
+ int32 remove_target = 3;
+ }
+
+ // Labels associated with this target change.
+ map<string, string> labels = 4;
+}
+
+// The response for [Firestore.Listen][google.firestore.v1beta1.Firestore.Listen].
+message ListenResponse {
+ // The supported responses.
+ oneof response_type {
+ // Targets have changed.
+ TargetChange target_change = 2;
+
+ // A [Document][google.firestore.v1beta1.Document] has changed.
+ DocumentChange document_change = 3;
+
+ // A [Document][google.firestore.v1beta1.Document] has been deleted.
+ DocumentDelete document_delete = 4;
+
+ // A [Document][google.firestore.v1beta1.Document] has been removed from a target (because it is no longer
+ // relevant to that target).
+ DocumentRemove document_remove = 6;
+
+ // A filter to apply to the set of documents previously returned for the
+ // given target.
+ //
+ // Returned when documents may have been removed from the given target, but
+ // the exact documents are unknown.
+ ExistenceFilter filter = 5;
+ }
+}
+
+// A specification of a set of documents to listen to.
+message Target {
+ // A target specified by a set of documents names.
+ message DocumentsTarget {
+ // The names of the documents to retrieve. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ // The request will fail if any of the document is not a child resource of
+ // the given `database`. Duplicate names will be elided.
+ repeated string documents = 2;
+ }
+
+ // A target specified by a query.
+ message QueryTarget {
+ // The parent resource name. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents` or
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ // For example:
+ // `projects/my-project/databases/my-database/documents` or
+ // `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom`
+ string parent = 1;
+
+ // The query to run.
+ oneof query_type {
+ // A structured query.
+ StructuredQuery structured_query = 2;
+ }
+ }
+
+ // The type of target to listen to.
+ oneof target_type {
+ // A target specified by a query.
+ QueryTarget query = 2;
+
+ // A target specified by a set of document names.
+ DocumentsTarget documents = 3;
+ }
+
+ // When to start listening.
+ //
+ // If not specified, all matching Documents are returned before any
+ // subsequent changes.
+ oneof resume_type {
+ // A resume token from a prior [TargetChange][google.firestore.v1beta1.TargetChange] for an identical target.
+ //
+ // Using a resume token with a different target is unsupported and may fail.
+ bytes resume_token = 4;
+
+ // Start listening after a specific `read_time`.
+ //
+ // The client must know the state of matching documents at this time.
+ google.protobuf.Timestamp read_time = 11;
+ }
+
+ // A client provided target ID.
+ //
+ // If not set, the server will assign an ID for the target.
+ //
+ // Used for resuming a target without changing IDs. The IDs can either be
+ // client-assigned or be server-assigned in a previous stream. All targets
+ // with client provided IDs must be added before adding a target that needs
+ // a server-assigned id.
+ int32 target_id = 5;
+
+ // If the target should be removed once it is current and consistent.
+ bool once = 6;
+}
+
+// Targets being watched have changed.
+message TargetChange {
+ // The type of change.
+ enum TargetChangeType {
+ // No change has occurred. Used only to send an updated `resume_token`.
+ NO_CHANGE = 0;
+
+ // The targets have been added.
+ ADD = 1;
+
+ // The targets have been removed.
+ REMOVE = 2;
+
+ // The targets reflect all changes committed before the targets were added
+ // to the stream.
+ //
+ // This will be sent after or with a `read_time` that is greater than or
+ // equal to the time at which the targets were added.
+ //
+ // Listeners can wait for this change if read-after-write semantics
+ // are desired.
+ CURRENT = 3;
+
+ // The targets have been reset, and a new initial state for the targets
+ // will be returned in subsequent changes.
+ //
+ // After the initial state is complete, `CURRENT` will be returned even
+ // if the target was previously indicated to be `CURRENT`.
+ RESET = 4;
+ }
+
+ // The type of change that occurred.
+ TargetChangeType target_change_type = 1;
+
+ // The target IDs of targets that have changed.
+ //
+ // If empty, the change applies to all targets.
+ //
+ // For `target_change_type=ADD`, the order of the target IDs matches the order
+ // of the requests to add the targets. This allows clients to unambiguously
+ // associate server-assigned target IDs with added targets.
+ //
+ // For other states, the order of the target IDs is not defined.
+ repeated int32 target_ids = 2;
+
+ // The error that resulted in this change, if applicable.
+ google.rpc.Status cause = 3;
+
+ // A token that can be used to resume the stream for the given `target_ids`,
+ // or all targets if `target_ids` is empty.
+ //
+ // Not set on every target change.
+ bytes resume_token = 4;
+
+ // The consistent `read_time` for the given `target_ids` (omitted when the
+ // target_ids are not at a consistent snapshot).
+ //
+ // The stream is guaranteed to send a `read_time` with `target_ids` empty
+ // whenever the entire stream reaches a new consistent snapshot. ADD,
+ // CURRENT, and RESET messages are guaranteed to (eventually) result in a
+ // new consistent snapshot (while NO_CHANGE and REMOVE messages are not).
+ //
+ // For a given stream, `read_time` is guaranteed to be monotonically
+ // increasing.
+ google.protobuf.Timestamp read_time = 6;
+}
+
+// The request for [Firestore.ListCollectionIds][google.firestore.v1beta1.Firestore.ListCollectionIds].
+message ListCollectionIdsRequest {
+ // The parent document. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ // For example:
+ // `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom`
+ string parent = 1;
+
+ // The maximum number of results to return.
+ int32 page_size = 2;
+
+ // A page token. Must be a value from
+ // [ListCollectionIdsResponse][google.firestore.v1beta1.ListCollectionIdsResponse].
+ string page_token = 3;
+}
+
+// The response from [Firestore.ListCollectionIds][google.firestore.v1beta1.Firestore.ListCollectionIds].
+message ListCollectionIdsResponse {
+ // The collection ids.
+ repeated string collection_ids = 1;
+
+ // A page token that may be used to continue the list.
+ string next_page_token = 2;
+}
diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/query.proto b/Firestore/Protos/protos/google/firestore/v1beta1/query.proto
new file mode 100644
index 0000000..d19b022
--- /dev/null
+++ b/Firestore/Protos/protos/google/firestore/v1beta1/query.proto
@@ -0,0 +1,231 @@
+// Copyright 2017 Google 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.
+
+syntax = "proto3";
+
+package google.firestore.v1beta1;
+
+import "google/api/annotations.proto";
+import "google/firestore/v1beta1/document.proto";
+import "google/protobuf/wrappers.proto";
+
+option csharp_namespace = "Google.Cloud.Firestore.V1Beta1";
+option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore";
+option java_multiple_files = true;
+option java_outer_classname = "QueryProto";
+option java_package = "com.google.firestore.v1beta1";
+option objc_class_prefix = "GCFS";
+
+
+// A Firestore query.
+message StructuredQuery {
+ // A selection of a collection, such as `messages as m1`.
+ message CollectionSelector {
+ // The collection ID.
+ // When set, selects only collections with this ID.
+ string collection_id = 2;
+
+ // When false, selects only collections that are immediate children of
+ // the `parent` specified in the containing `RunQueryRequest`.
+ // When true, selects all descendant collections.
+ bool all_descendants = 3;
+ }
+
+ // A filter.
+ message Filter {
+ // The type of filter.
+ oneof filter_type {
+ // A composite filter.
+ CompositeFilter composite_filter = 1;
+
+ // A filter on a document field.
+ FieldFilter field_filter = 2;
+
+ // A filter that takes exactly one argument.
+ UnaryFilter unary_filter = 3;
+ }
+ }
+
+ // A filter that merges multiple other filters using the given operator.
+ message CompositeFilter {
+ // A composite filter operator.
+ enum Operator {
+ // Unspecified. This value must not be used.
+ OPERATOR_UNSPECIFIED = 0;
+
+ // The results are required to satisfy each of the combined filters.
+ AND = 1;
+ }
+
+ // The operator for combining multiple filters.
+ Operator op = 1;
+
+ // The list of filters to combine.
+ // Must contain at least one filter.
+ repeated Filter filters = 2;
+ }
+
+ // A filter on a specific field.
+ message FieldFilter {
+ // A field filter operator.
+ enum Operator {
+ // Unspecified. This value must not be used.
+ OPERATOR_UNSPECIFIED = 0;
+
+ // Less than. Requires that the field come first in `order_by`.
+ LESS_THAN = 1;
+
+ // Less than or equal. Requires that the field come first in `order_by`.
+ LESS_THAN_OR_EQUAL = 2;
+
+ // Greater than. Requires that the field come first in `order_by`.
+ GREATER_THAN = 3;
+
+ // Greater than or equal. Requires that the field come first in
+ // `order_by`.
+ GREATER_THAN_OR_EQUAL = 4;
+
+ // Equal.
+ EQUAL = 5;
+ }
+
+ // The field to filter by.
+ FieldReference field = 1;
+
+ // The operator to filter by.
+ Operator op = 2;
+
+ // The value to compare to.
+ Value value = 3;
+ }
+
+ // A filter with a single operand.
+ message UnaryFilter {
+ // A unary operator.
+ enum Operator {
+ // Unspecified. This value must not be used.
+ OPERATOR_UNSPECIFIED = 0;
+
+ // Test if a field is equal to NaN.
+ IS_NAN = 2;
+
+ // Test if an exprestion evaluates to Null.
+ IS_NULL = 3;
+ }
+
+ // The unary operator to apply.
+ Operator op = 1;
+
+ // The argument to the filter.
+ oneof operand_type {
+ // The field to which to apply the operator.
+ FieldReference field = 2;
+ }
+ }
+
+ // An order on a field.
+ message Order {
+ // The field to order by.
+ FieldReference field = 1;
+
+ // The direction to order by. Defaults to `ASCENDING`.
+ Direction direction = 2;
+ }
+
+ // A reference to a field, such as `max(messages.time) as max_time`.
+ message FieldReference {
+ string field_path = 2;
+ }
+
+ // The projection of document's fields to return.
+ message Projection {
+ // The fields to return.
+ //
+ // If empty, all fields are returned. To only return the name
+ // of the document, use `['__name__']`.
+ repeated FieldReference fields = 2;
+ }
+
+ // A sort direction.
+ enum Direction {
+ // Unspecified.
+ DIRECTION_UNSPECIFIED = 0;
+
+ // Ascending.
+ ASCENDING = 1;
+
+ // Descending.
+ DESCENDING = 2;
+ }
+
+ // The projection to return.
+ Projection select = 1;
+
+ // The collections to query.
+ repeated CollectionSelector from = 2;
+
+ // The filter to apply.
+ Filter where = 3;
+
+ // The order to apply to the query results.
+ //
+ // Firestore guarantees a stable ordering through the following rules:
+ //
+ // * Any field required to appear in `order_by`, that is not already
+ // specified in `order_by`, is appended to the order in field name order
+ // by default.
+ // * If an order on `__name__` is not specified, it is appended by default.
+ //
+ // Fields are appended with the same sort direction as the last order
+ // specified, or 'ASCENDING' if no order was specified. For example:
+ //
+ // * `SELECT * FROM Foo ORDER BY A` becomes
+ // `SELECT * FROM Foo ORDER BY A, __name__`
+ // * `SELECT * FROM Foo ORDER BY A DESC` becomes
+ // `SELECT * FROM Foo ORDER BY A DESC, __name__ DESC`
+ // * `SELECT * FROM Foo WHERE A > 1` becomes
+ // `SELECT * FROM Foo WHERE A > 1 ORDER BY A, __name__`
+ repeated Order order_by = 4;
+
+ // A starting point for the query results.
+ Cursor start_at = 7;
+
+ // A end point for the query results.
+ Cursor end_at = 8;
+
+ // The number of results to skip.
+ //
+ // Applies before limit, but after all other constraints. Must be >= 0 if
+ // specified.
+ int32 offset = 6;
+
+ // The maximum number of results to return.
+ //
+ // Applies after all other constraints.
+ // Must be >= 0 if specified.
+ google.protobuf.Int32Value limit = 5;
+}
+
+// A position in a query result set.
+message Cursor {
+ // The values that represent a position, in the order they appear in
+ // the order by clause of a query.
+ //
+ // Can contain fewer values than specified in the order by clause.
+ repeated Value values = 1;
+
+ // If the position is just before or just after the given values, relative
+ // to the sort order defined by the query.
+ bool before = 2;
+}
diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/write.proto b/Firestore/Protos/protos/google/firestore/v1beta1/write.proto
new file mode 100644
index 0000000..b6e9d5f
--- /dev/null
+++ b/Firestore/Protos/protos/google/firestore/v1beta1/write.proto
@@ -0,0 +1,189 @@
+// Copyright 2017 Google 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.
+
+syntax = "proto3";
+
+package google.firestore.v1beta1;
+
+import "google/api/annotations.proto";
+import "google/firestore/v1beta1/common.proto";
+import "google/firestore/v1beta1/document.proto";
+import "google/protobuf/timestamp.proto";
+
+option csharp_namespace = "Google.Cloud.Firestore.V1Beta1";
+option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore";
+option java_multiple_files = true;
+option java_outer_classname = "WriteProto";
+option java_package = "com.google.firestore.v1beta1";
+option objc_class_prefix = "GCFS";
+
+
+// A write on a document.
+message Write {
+ // The operation to execute.
+ oneof operation {
+ // A document to write.
+ Document update = 1;
+
+ // A document name to delete. In the format:
+ // `projects/{project_id}/databases/{database_id}/documents/{document_path}`.
+ string delete = 2;
+
+ // The name of a document on which to verify the `current_document`
+ // precondition.
+ // This only requires read access to the document.
+ string verify = 5;
+
+ // Applies a tranformation to a document.
+ // At most one `transform` per document is allowed in a given request.
+ // An `update` cannot follow a `transform` on the same document in a given
+ // request.
+ DocumentTransform transform = 6;
+ }
+
+ // The fields to update in this write.
+ //
+ // This field can be set only when the operation is `update`.
+ // None of the field paths in the mask may contain a reserved name.
+ // If the document exists on the server and has fields not referenced in the
+ // mask, they are left unchanged.
+ // Fields referenced in the mask, but not present in the input document, are
+ // deleted from the document on the server.
+ // The field paths in this mask must not contain a reserved field name.
+ DocumentMask update_mask = 3;
+
+ // An optional precondition on the document.
+ //
+ // The write will fail if this is set and not met by the target document.
+ Precondition current_document = 4;
+}
+
+// A transformation of a document.
+message DocumentTransform {
+ // A transformation of a field of the document.
+ message FieldTransform {
+ // A value that is calculated by the server.
+ enum ServerValue {
+ // Unspecified. This value must not be used.
+ SERVER_VALUE_UNSPECIFIED = 0;
+
+ // The time at which the server processed the request.
+ REQUEST_TIME = 1;
+ }
+
+ // The path of the field. See [Document.fields][google.firestore.v1beta1.Document.fields] for the field path syntax
+ // reference.
+ string field_path = 1;
+
+ // The transformation to apply on the field.
+ oneof transform_type {
+ // Sets the field to the given server value.
+ ServerValue set_to_server_value = 2;
+ }
+ }
+
+ // The name of the document to transform.
+ string document = 1;
+
+ // The list of transformations to apply to the fields of the document, in
+ // order.
+ repeated FieldTransform field_transforms = 2;
+}
+
+// The result of applying a write.
+message WriteResult {
+ // The last update time of the document after applying the write. Not set
+ // after a `delete`.
+ //
+ // If the write did not actually change the document, this will be the
+ // previous update_time.
+ google.protobuf.Timestamp update_time = 1;
+
+ // The results of applying each [DocumentTransform.FieldTransform][google.firestore.v1beta1.DocumentTransform.FieldTransform], in the
+ // same order.
+ repeated Value transform_results = 2;
+}
+
+// A [Document][google.firestore.v1beta1.Document] has changed.
+//
+// May be the result of multiple [writes][google.firestore.v1beta1.Write], including deletes, that
+// ultimately resulted in a new value for the [Document][google.firestore.v1beta1.Document].
+//
+// Multiple [DocumentChange][google.firestore.v1beta1.DocumentChange] messages may be returned for the same logical
+// change, if multiple targets are affected.
+message DocumentChange {
+ // The new state of the [Document][google.firestore.v1beta1.Document].
+ //
+ // If `mask` is set, contains only fields that were updated or added.
+ Document document = 1;
+
+ // A set of target IDs of targets that match this document.
+ repeated int32 target_ids = 5;
+
+ // A set of target IDs for targets that no longer match this document.
+ repeated int32 removed_target_ids = 6;
+}
+
+// A [Document][google.firestore.v1beta1.Document] has been deleted.
+//
+// May be the result of multiple [writes][google.firestore.v1beta1.Write], including updates, the
+// last of which deleted the [Document][google.firestore.v1beta1.Document].
+//
+// Multiple [DocumentDelete][google.firestore.v1beta1.DocumentDelete] messages may be returned for the same logical
+// delete, if multiple targets are affected.
+message DocumentDelete {
+ // The resource name of the [Document][google.firestore.v1beta1.Document] that was deleted.
+ string document = 1;
+
+ // A set of target IDs for targets that previously matched this entity.
+ repeated int32 removed_target_ids = 6;
+
+ // The read timestamp at which the delete was observed.
+ //
+ // Greater or equal to the `commit_time` of the delete.
+ google.protobuf.Timestamp read_time = 4;
+}
+
+// A [Document][google.firestore.v1beta1.Document] has been removed from the view of the targets.
+//
+// Sent if the document is no longer relevant to a target and is out of view.
+// Can be sent instead of a DocumentDelete or a DocumentChange if the server
+// can not send the new value of the document.
+//
+// Multiple [DocumentRemove][google.firestore.v1beta1.DocumentRemove] messages may be returned for the same logical
+// write or delete, if multiple targets are affected.
+message DocumentRemove {
+ // The resource name of the [Document][google.firestore.v1beta1.Document] that has gone out of view.
+ string document = 1;
+
+ // A set of target IDs for targets that previously matched this document.
+ repeated int32 removed_target_ids = 2;
+
+ // The read timestamp at which the remove was observed.
+ //
+ // Greater or equal to the `commit_time` of the change/delete/remove.
+ google.protobuf.Timestamp read_time = 4;
+}
+
+// A digest of all the documents that match a given target.
+message ExistenceFilter {
+ // The target ID to which this filter applies.
+ int32 target_id = 1;
+
+ // The total count of documents that match [target_id][google.firestore.v1beta1.ExistenceFilter.target_id].
+ //
+ // If different from the count of documents in the client that match, the
+ // client must manually determine which documents no longer match the target.
+ int32 count = 2;
+}
diff --git a/Firestore/Protos/protos/google/rpc/status.proto b/Firestore/Protos/protos/google/rpc/status.proto
new file mode 100644
index 0000000..0839ee9
--- /dev/null
+++ b/Firestore/Protos/protos/google/rpc/status.proto
@@ -0,0 +1,92 @@
+// Copyright 2017 Google 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.
+
+syntax = "proto3";
+
+package google.rpc;
+
+import "google/protobuf/any.proto";
+
+option go_package = "google.golang.org/genproto/googleapis/rpc/status;status";
+option java_multiple_files = true;
+option java_outer_classname = "StatusProto";
+option java_package = "com.google.rpc";
+option objc_class_prefix = "RPC";
+
+
+// The `Status` type defines a logical error model that is suitable for different
+// programming environments, including REST APIs and RPC APIs. It is used by
+// [gRPC](https://github.com/grpc). The error model is designed to be:
+//
+// - Simple to use and understand for most users
+// - Flexible enough to meet unexpected needs
+//
+// # Overview
+//
+// The `Status` message contains three pieces of data: error code, error message,
+// and error details. The error code should be an enum value of
+// [google.rpc.Code][google.rpc.Code], but it may accept additional error codes if needed. The
+// error message should be a developer-facing English message that helps
+// developers *understand* and *resolve* the error. If a localized user-facing
+// error message is needed, put the localized message in the error details or
+// localize it in the client. The optional error details may contain arbitrary
+// information about the error. There is a predefined set of error detail types
+// in the package `google.rpc` that can be used for common error conditions.
+//
+// # Language mapping
+//
+// The `Status` message is the logical representation of the error model, but it
+// is not necessarily the actual wire format. When the `Status` message is
+// exposed in different client libraries and different wire protocols, it can be
+// mapped differently. For example, it will likely be mapped to some exceptions
+// in Java, but more likely mapped to some error codes in C.
+//
+// # Other uses
+//
+// The error model and the `Status` message can be used in a variety of
+// environments, either with or without APIs, to provide a
+// consistent developer experience across different environments.
+//
+// Example uses of this error model include:
+//
+// - Partial errors. If a service needs to return partial errors to the client,
+// it may embed the `Status` in the normal response to indicate the partial
+// errors.
+//
+// - Workflow errors. A typical workflow has multiple steps. Each step may
+// have a `Status` message for error reporting.
+//
+// - Batch operations. If a client uses batch request and batch response, the
+// `Status` message should be used directly inside batch response, one for
+// each error sub-response.
+//
+// - Asynchronous operations. If an API call embeds asynchronous operation
+// results in its response, the status of those operations should be
+// represented directly using the `Status` message.
+//
+// - Logging. If some API errors are stored in logs, the message `Status` could
+// be used directly after any stripping needed for security/privacy reasons.
+message Status {
+ // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
+ int32 code = 1;
+
+ // A developer-facing error message, which should be in English. Any
+ // user-facing error message should be localized and sent in the
+ // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
+ string message = 2;
+
+ // A list of messages that carry the error details. There is a common set of
+ // message types for APIs to use.
+ repeated google.protobuf.Any details = 3;
+}
diff --git a/Firestore/Protos/protos/google/type/latlng.proto b/Firestore/Protos/protos/google/type/latlng.proto
new file mode 100644
index 0000000..4e8c65d
--- /dev/null
+++ b/Firestore/Protos/protos/google/type/latlng.proto
@@ -0,0 +1,71 @@
+// Copyright 2016 Google 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.
+
+syntax = "proto3";
+
+package google.type;
+
+option go_package = "google.golang.org/genproto/googleapis/type/latlng;latlng";
+option java_multiple_files = true;
+option java_outer_classname = "LatLngProto";
+option java_package = "com.google.type";
+option objc_class_prefix = "GTP";
+
+
+// An object representing a latitude/longitude pair. This is expressed as a pair
+// of doubles representing degrees latitude and degrees longitude. Unless
+// specified otherwise, this must conform to the
+// <a href="http://www.unoosa.org/pdf/icg/2012/template/WGS_84.pdf">WGS84
+// standard</a>. Values must be within normalized ranges.
+//
+// Example of normalization code in Python:
+//
+// def NormalizeLongitude(longitude):
+// """Wraps decimal degrees longitude to [-180.0, 180.0]."""
+// q, r = divmod(longitude, 360.0)
+// if r > 180.0 or (r == 180.0 and q <= -1.0):
+// return r - 360.0
+// return r
+//
+// def NormalizeLatLng(latitude, longitude):
+// """Wraps decimal degrees latitude and longitude to
+// [-90.0, 90.0] and [-180.0, 180.0], respectively."""
+// r = latitude % 360.0
+// if r <= 90.0:
+// return r, NormalizeLongitude(longitude)
+// elif r >= 270.0:
+// return r - 360, NormalizeLongitude(longitude)
+// else:
+// return 180 - r, NormalizeLongitude(longitude + 180.0)
+//
+// assert 180.0 == NormalizeLongitude(180.0)
+// assert -180.0 == NormalizeLongitude(-180.0)
+// assert -179.0 == NormalizeLongitude(181.0)
+// assert (0.0, 0.0) == NormalizeLatLng(360.0, 0.0)
+// assert (0.0, 0.0) == NormalizeLatLng(-360.0, 0.0)
+// assert (85.0, 180.0) == NormalizeLatLng(95.0, 0.0)
+// assert (-85.0, -170.0) == NormalizeLatLng(-95.0, 10.0)
+// assert (90.0, 10.0) == NormalizeLatLng(90.0, 10.0)
+// assert (-90.0, -10.0) == NormalizeLatLng(-90.0, -10.0)
+// assert (0.0, -170.0) == NormalizeLatLng(-180.0, 10.0)
+// assert (0.0, -170.0) == NormalizeLatLng(180.0, 10.0)
+// assert (-90.0, 10.0) == NormalizeLatLng(270.0, 10.0)
+// assert (90.0, 10.0) == NormalizeLatLng(-270.0, 10.0)
+message LatLng {
+ // The latitude in degrees. It must be in the range [-90.0, +90.0].
+ double latitude = 1;
+
+ // The longitude in degrees. It must be in the range [-180.0, +180.0].
+ double longitude = 2;
+}
diff --git a/Firestore/Protos/strip-registry.py b/Firestore/Protos/strip-registry.py
new file mode 100755
index 0000000..cd48784
--- /dev/null
+++ b/Firestore/Protos/strip-registry.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+
+# Copyright 2017 Google
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""strip-registry.py removes extensionRegistry functions from objc protos.
+"""
+
+import sys
+
+filename = sys.argv[1]
+
+with open(filename) as input:
+ content = [x.strip('\n') for x in input.readlines()]
+
+if '+ (GPBExtensionRegistry*)extensionRegistry {' in content:
+ new_content = []
+ skip = False
+ for line in content:
+ if '+ (GPBExtensionRegistry*)extensionRegistry {' in line:
+ skip = True
+ if not skip:
+ new_content.append(line)
+ elif line == '}':
+ skip = False
+
+ with open(filename, "w") as output:
+ output.write('\n'.join(new_content) + '\n')
diff --git a/Firestore/README.md b/Firestore/README.md
new file mode 100644
index 0000000..fbff1b1
--- /dev/null
+++ b/Firestore/README.md
@@ -0,0 +1,15 @@
+## Usage
+
+```
+$ cd Firestore/Example
+$ pod update
+$ open Firebase.xcworkspace
+Select the FirestoreTests scheme
+⌘-u to build and run the unit tests
+```
+
+### Building Protos
+
+Typically you should not need to worrying about regenerating the Objective-C
+files from the .proto files. If you do, see instructions at
+[Protos/README.md](Protos/README.md).
diff --git a/Firestore/Source/API/FIRCollectionReference+Internal.h b/Firestore/Source/API/FIRCollectionReference+Internal.h
new file mode 100644
index 0000000..1d00cbb
--- /dev/null
+++ b/Firestore/Source/API/FIRCollectionReference+Internal.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 "FIRCollectionReference.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FSTResourcePath;
+
+/** Internal FIRCollectionReference API we don't want exposed in our public header files. */
+@interface FIRCollectionReference (Internal)
++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRCollectionReference.m b/Firestore/Source/API/FIRCollectionReference.m
new file mode 100644
index 0000000..1ded4d2
--- /dev/null
+++ b/Firestore/Source/API/FIRCollectionReference.m
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRCollectionReference.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRQuery+Internal.h"
+#import "FIRQuery_Init.h"
+#import "FSTAssert.h"
+#import "FSTDocumentKey.h"
+#import "FSTPath.h"
+#import "FSTQuery.h"
+#import "FSTUsageValidation.h"
+#import "FSTUtil.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRCollectionReference ()
+- (instancetype)initWithPath:(FSTResourcePath *)path
+ firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER;
+
+// Mark the super class designated initializer unavailable.
+- (instancetype)initWithQuery:(FSTQuery *)query
+ firestore:(FIRFirestore *)firestore
+ __attribute__((unavailable("Use the initWithPath constructor of FIRCollectionReference.")));
+@end
+
+@implementation FIRCollectionReference (Internal)
++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore {
+ return [[FIRCollectionReference alloc] initWithPath:path firestore:firestore];
+}
+@end
+
+@implementation FIRCollectionReference
+
+- (instancetype)initWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore {
+ if (path.length % 2 != 1) {
+ FSTThrowInvalidArgument(
+ @"Invalid collection reference. Collection references must have an odd "
+ "number of segments, but %@ has %d",
+ path.canonicalString, path.length);
+ }
+ self = [super initWithQuery:[FSTQuery queryWithPath:path] firestore:firestore];
+ return self;
+}
+
+// Override the designated initializer from the super class.
+- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore {
+ FSTFail(@"Use FIRCollectionReference initWithPath: initializer.");
+}
+
+- (NSString *)collectionID {
+ return [self.query.path lastSegment];
+}
+
+- (FIRDocumentReference *_Nullable)parent {
+ FSTResourcePath *parentPath = [self.query.path pathByRemovingLastSegment];
+ if (parentPath.isEmpty) {
+ return nil;
+ } else {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:parentPath];
+ return [FIRDocumentReference referenceWithKey:key firestore:self.firestore];
+ }
+}
+
+- (NSString *)path {
+ return [self.query.path canonicalString];
+}
+
+- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath {
+ if (!documentPath) {
+ FSTThrowInvalidArgument(@"Document path cannot be nil.");
+ }
+ FSTResourcePath *subPath = [FSTResourcePath pathWithString:documentPath];
+ FSTResourcePath *path = [self.query.path pathByAppendingPath:subPath];
+ return [FIRDocumentReference referenceWithPath:path firestore:self.firestore];
+}
+
+- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data {
+ return [self addDocumentWithData:data completion:nil];
+}
+
+- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data
+ completion:
+ (nullable void (^)(NSError *_Nullable error))completion {
+ FIRDocumentReference *docRef = [self documentWithAutoID];
+ [docRef setData:data completion:completion];
+ return docRef;
+}
+
+- (FIRDocumentReference *)documentWithAutoID {
+ NSString *autoID = [FSTUtil autoID];
+ FSTDocumentKey *key =
+ [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:autoID]];
+ return [FIRDocumentReference referenceWithKey:key firestore:self.firestore];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentChange+Internal.h b/Firestore/Source/API/FIRDocumentChange+Internal.h
new file mode 100644
index 0000000..7e2e5c6
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentChange+Internal.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 "FIRDocumentChange.h"
+
+@class FSTViewSnapshot;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRDocumentChange API we don't want exposed in our public header files. */
+@interface FIRDocumentChange (Internal)
+
+/** Calculates the array of FIRDocumentChange's based on the given FSTViewSnapshot. */
++ (NSArray<FIRDocumentChange *> *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot
+ firestore:(FIRFirestore *)firestore;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentChange.m b/Firestore/Source/API/FIRDocumentChange.m
new file mode 100644
index 0000000..f284bfe
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentChange.m
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDocumentChange.h"
+
+#import "FIRDocumentSnapshot+Internal.h"
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentSet.h"
+#import "FSTQuery.h"
+#import "FSTViewSnapshot.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRDocumentChange ()
+
+- (instancetype)initWithType:(FIRDocumentChangeType)type
+ document:(FIRDocumentSnapshot *)document
+ oldIndex:(NSUInteger)oldIndex
+ newIndex:(NSUInteger)newIndex NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FIRDocumentChange (Internal)
+
++ (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)change {
+ if (change.type == FSTDocumentViewChangeTypeAdded) {
+ return FIRDocumentChangeTypeAdded;
+ } else if (change.type == FSTDocumentViewChangeTypeModified ||
+ change.type == FSTDocumentViewChangeTypeMetadata) {
+ return FIRDocumentChangeTypeModified;
+ } else if (change.type == FSTDocumentViewChangeTypeRemoved) {
+ return FIRDocumentChangeTypeRemoved;
+ } else {
+ FSTFail(@"Unknown FSTDocumentViewChange: %ld", (long)change.type);
+ }
+}
+
++ (NSArray<FIRDocumentChange *> *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot
+ firestore:(FIRFirestore *)firestore {
+ if (snapshot.oldDocuments.isEmpty) {
+ // Special case the first snapshot because index calculation is easy and fast
+ FSTDocument *_Nullable lastDocument = nil;
+ NSUInteger index = 0;
+ NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array];
+ for (FSTDocumentViewChange *change in snapshot.documentChanges) {
+ FIRDocumentSnapshot *document =
+ [FIRDocumentSnapshot snapshotWithFirestore:firestore
+ documentKey:change.document.key
+ document:change.document
+ fromCache:snapshot.isFromCache];
+ FSTAssert(change.type == FSTDocumentViewChangeTypeAdded,
+ @"Invalid event type for first snapshot");
+ FSTAssert(!lastDocument ||
+ snapshot.query.comparator(lastDocument, change.document) == NSOrderedAscending,
+ @"Got added events in wrong order");
+ [changes addObject:[[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeAdded
+ document:document
+ oldIndex:NSNotFound
+ newIndex:index++]];
+ }
+ return changes;
+ } else {
+ // A DocumentSet that is updated incrementally as changes are applied to use to lookup the index
+ // of a document.
+ FSTDocumentSet *indexTracker = snapshot.oldDocuments;
+ NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array];
+ for (FSTDocumentViewChange *change in snapshot.documentChanges) {
+ FIRDocumentSnapshot *document =
+ [FIRDocumentSnapshot snapshotWithFirestore:firestore
+ documentKey:change.document.key
+ document:change.document
+ fromCache:snapshot.isFromCache];
+
+ NSUInteger oldIndex = NSNotFound;
+ NSUInteger newIndex = NSNotFound;
+ if (change.type != FSTDocumentViewChangeTypeAdded) {
+ oldIndex = [indexTracker indexOfKey:change.document.key];
+ FSTAssert(oldIndex != NSNotFound, @"Index for document not found");
+ indexTracker = [indexTracker documentSetByRemovingKey:change.document.key];
+ }
+ if (change.type != FSTDocumentViewChangeTypeRemoved) {
+ indexTracker = [indexTracker documentSetByAddingDocument:change.document];
+ newIndex = [indexTracker indexOfKey:change.document.key];
+ }
+ [FIRDocumentChange documentChangeTypeForChange:change];
+ FIRDocumentChangeType type = [FIRDocumentChange documentChangeTypeForChange:change];
+ [changes addObject:[[FIRDocumentChange alloc] initWithType:type
+ document:document
+ oldIndex:oldIndex
+ newIndex:newIndex]];
+ }
+ return changes;
+ }
+}
+
+@end
+
+@implementation FIRDocumentChange
+
+- (instancetype)initWithType:(FIRDocumentChangeType)type
+ document:(FIRDocumentSnapshot *)document
+ oldIndex:(NSUInteger)oldIndex
+ newIndex:(NSUInteger)newIndex {
+ if (self = [super init]) {
+ _type = type;
+ _document = document;
+ _oldIndex = oldIndex;
+ _newIndex = newIndex;
+ }
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentReference+Internal.h b/Firestore/Source/API/FIRDocumentReference+Internal.h
new file mode 100644
index 0000000..5e12ddc
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentReference+Internal.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 "FIRDocumentReference.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FSTDocumentKey;
+@class FSTResourcePath;
+
+/** Internal FIRDocumentReference API we don't want exposed in our public header files. */
+@interface FIRDocumentReference (Internal)
+
++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore;
++ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore;
+
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentReference.m b/Firestore/Source/API/FIRDocumentReference.m
new file mode 100644
index 0000000..5515bd6
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentReference.m
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDocumentReference.h"
+
+#import "FIRCollectionReference+Internal.h"
+#import "FIRDocumentReference+Internal.h"
+#import "FIRDocumentSnapshot+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRFirestoreErrors.h"
+#import "FIRListenerRegistration+Internal.h"
+#import "FIRSetOptions+Internal.h"
+#import "FIRSnapshotMetadata.h"
+#import "FSTAssert.h"
+#import "FSTAsyncQueryListener.h"
+#import "FSTDocumentKey.h"
+#import "FSTDocumentSet.h"
+#import "FSTEventManager.h"
+#import "FSTFieldValue.h"
+#import "FSTFirestoreClient.h"
+#import "FSTMutation.h"
+#import "FSTPath.h"
+#import "FSTQuery.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FIRDocumentListenOptions
+
+@interface FIRDocumentListenOptions ()
+
+- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FIRDocumentListenOptions
+
++ (instancetype)options {
+ return [[FIRDocumentListenOptions alloc] init];
+}
+
+- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges {
+ if (self = [super init]) {
+ _includeMetadataChanges = includeMetadataChanges;
+ }
+ return self;
+}
+
+- (instancetype)init {
+ return [self initWithIncludeMetadataChanges:NO];
+}
+
+- (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges {
+ return [[FIRDocumentListenOptions alloc] initWithIncludeMetadataChanges:includeMetadataChanges];
+}
+
+@end
+
+#pragma mark - FIRDocumentReference
+
+@interface FIRDocumentReference ()
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER;
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+@end
+
+@implementation FIRDocumentReference (Internal)
+
++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore {
+ if (path.length % 2 != 0) {
+ FSTThrowInvalidArgument(
+ @"Invalid document reference. Document references must have an even "
+ "number of segments, but %@ has %d",
+ path.canonicalString, path.length);
+ }
+ return
+ [FIRDocumentReference referenceWithKey:[FSTDocumentKey keyWithPath:path] firestore:firestore];
+}
+
++ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore {
+ return [[FIRDocumentReference alloc] initWithKey:key firestore:firestore];
+}
+
+@end
+
+@implementation FIRDocumentReference
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore {
+ if (self = [super init]) {
+ _key = key;
+ _firestore = firestore;
+ }
+ return self;
+}
+
+- (NSString *)documentID {
+ return [self.key.path lastSegment];
+}
+
+- (FIRCollectionReference *)parent {
+ FSTResourcePath *parentPath = [self.key.path pathByRemovingLastSegment];
+ return [FIRCollectionReference referenceWithPath:parentPath firestore:self.firestore];
+}
+
+- (NSString *)path {
+ return [self.key.path canonicalString];
+}
+
+- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath {
+ if (!collectionPath) {
+ FSTThrowInvalidArgument(@"Collection path cannot be nil.");
+ }
+ FSTResourcePath *subPath = [FSTResourcePath pathWithString:collectionPath];
+ FSTResourcePath *path = [self.key.path pathByAppendingPath:subPath];
+ return [FIRCollectionReference referenceWithPath:path firestore:self.firestore];
+}
+
+- (void)setData:(NSDictionary<NSString *, id> *)documentData {
+ return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil];
+}
+
+- (void)setData:(NSDictionary<NSString *, id> *)documentData options:(FIRSetOptions *)options {
+ return [self setData:documentData options:options completion:nil];
+}
+
+- (void)setData:(NSDictionary<NSString *, id> *)documentData
+ completion:(nullable void (^)(NSError *_Nullable error))completion {
+ return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion];
+}
+
+- (void)setData:(NSDictionary<NSString *, id> *)documentData
+ options:(FIRSetOptions *)options
+ completion:(nullable void (^)(NSError *_Nullable error))completion {
+ FSTParsedSetData *parsed =
+ [self.firestore.dataConverter parsedSetData:documentData options:options];
+ return [self.firestore.client
+ writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]]
+ completion:completion];
+}
+
+- (void)updateData:(NSDictionary<id, id> *)fields {
+ return [self updateData:fields completion:nil];
+}
+
+- (void)updateData:(NSDictionary<id, id> *)fields
+ completion:(nullable void (^)(NSError *_Nullable error))completion {
+ FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields];
+ return [self.firestore.client
+ writeMutations:[parsed mutationsWithKey:self.key
+ precondition:[FSTPrecondition preconditionWithExists:YES]]
+ completion:completion];
+}
+
+- (void)deleteDocument {
+ return [self deleteDocumentWithCompletion:nil];
+}
+
+- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion {
+ FSTDeleteMutation *mutation =
+ [[FSTDeleteMutation alloc] initWithKey:self.key precondition:[FSTPrecondition none]];
+ return [self.firestore.client writeMutations:@[ mutation ] completion:completion];
+}
+
+- (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document,
+ NSError *_Nullable error))completion {
+ FSTListenOptions *listenOptions =
+ [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES
+ includeDocumentMetadataChanges:YES
+ waitForSyncWhenOnline:YES];
+
+ dispatch_semaphore_t registered = dispatch_semaphore_create(0);
+ __block id<FIRListenerRegistration> listenerRegistration;
+ FIRDocumentSnapshotBlock listener = ^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+
+ // Remove query first before passing event to user to avoid user actions affecting the
+ // now stale query.
+ dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER);
+ [listenerRegistration remove];
+
+ if (!snapshot.exists && snapshot.metadata.fromCache) {
+ // TODO(dimond): Reconsider how to raise missing documents when offline.
+ // If we're online and the document doesn't exist then we call the completion with
+ // a document with document.exists set to false. If we're offline however, we call the
+ // completion handler with an error. Two options:
+ // 1) Cache the negative response from the server so we can deliver that even when you're
+ // offline.
+ // 2) Actually call the completion handler with an error if the document doesn't exist when
+ // you are offline.
+ // TODO(dimond): Use proper error domain
+ completion(nil,
+ [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeUnavailable
+ userInfo:@{
+ NSLocalizedDescriptionKey :
+ @"Failed to get document because the client is offline.",
+ }]);
+ } else {
+ completion(snapshot, nil);
+ }
+ };
+
+ listenerRegistration =
+ [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener];
+ dispatch_semaphore_signal(registered);
+}
+
+- (id<FIRListenerRegistration>)addSnapshotListener:(FIRDocumentSnapshotBlock)listener {
+ return [self addSnapshotListenerWithOptions:nil listener:listener];
+}
+
+- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions:
+ (nullable FIRDocumentListenOptions *)options
+ listener:(FIRDocumentSnapshotBlock)listener {
+ return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options]
+ listener:listener];
+}
+
+- (id<FIRListenerRegistration>)
+addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions
+ listener:(FIRDocumentSnapshotBlock)listener {
+ FIRFirestore *firestore = self.firestore;
+ FSTQuery *query = [FSTQuery queryWithPath:self.key.path];
+ FSTDocumentKey *key = self.key;
+
+ FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) {
+ if (error) {
+ listener(nil, error);
+ return;
+ }
+
+ FSTAssert(snapshot.documents.count <= 1, @"Too many document returned on a document query");
+ FSTDocument *document = [snapshot.documents documentForKey:key];
+
+ FIRDocumentSnapshot *result = [FIRDocumentSnapshot snapshotWithFirestore:firestore
+ documentKey:key
+ document:document
+ fromCache:snapshot.fromCache];
+ listener(result, nil);
+ };
+
+ FSTAsyncQueryListener *asyncListener =
+ [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue
+ snapshotHandler:snapshotHandler];
+
+ FSTQueryListener *internalListener =
+ [firestore.client listenToQuery:query
+ options:internalOptions
+ viewSnapshotHandler:[asyncListener asyncSnapshotHandler]];
+ return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client
+ asyncListener:asyncListener
+ internalListener:internalListener];
+}
+
+/** Converts the public API options object to the internal options object. */
+- (FSTListenOptions *)internalOptions:(nullable FIRDocumentListenOptions *)options {
+ return
+ [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:options.includeMetadataChanges
+ includeDocumentMetadataChanges:options.includeMetadataChanges
+ waitForSyncWhenOnline:NO];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentSnapshot+Internal.h b/Firestore/Source/API/FIRDocumentSnapshot+Internal.h
new file mode 100644
index 0000000..f2776f0
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentSnapshot+Internal.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 "FIRDocumentSnapshot.h"
+
+@class FIRFirestore;
+@class FSTDocument;
+@class FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRDocumentSnapshot API we don't want exposed in our public header files. */
+@interface FIRDocumentSnapshot (Internal)
+
++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore
+ documentKey:(FSTDocumentKey *)documentKey
+ document:(nullable FSTDocument *)document
+ fromCache:(BOOL)fromCache;
+
+@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m
new file mode 100644
index 0000000..b5f61ba
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentSnapshot.m
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRDocumentSnapshot.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRFieldPath+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRSnapshotMetadata+Internal.h"
+#import "FSTDatabaseID.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTFieldValue.h"
+#import "FSTPath.h"
+#import "FSTUsageValidation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRDocumentSnapshot ()
+
+- (instancetype)initWithFirestore:(FIRFirestore *)firestore
+ documentKey:(FSTDocumentKey *)documentKey
+ document:(nullable FSTDocument *)document
+ fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FIRFirestore *firestore;
+@property(nonatomic, strong, readonly) FSTDocumentKey *internalKey;
+@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument;
+@property(nonatomic, assign, readonly) BOOL fromCache;
+
+@end
+
+@implementation FIRDocumentSnapshot (Internal)
+
++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore
+ documentKey:(FSTDocumentKey *)documentKey
+ document:(nullable FSTDocument *)document
+ fromCache:(BOOL)fromCache {
+ return [[FIRDocumentSnapshot alloc] initWithFirestore:firestore
+ documentKey:documentKey
+ document:document
+ fromCache:fromCache];
+}
+
+@end
+
+@implementation FIRDocumentSnapshot {
+ FIRSnapshotMetadata *_cachedMetadata;
+}
+
+@dynamic metadata;
+
+- (instancetype)initWithFirestore:(FIRFirestore *)firestore
+ documentKey:(FSTDocumentKey *)documentKey
+ document:(nullable FSTDocument *)document
+ fromCache:(BOOL)fromCache {
+ if (self = [super init]) {
+ _firestore = firestore;
+ _internalKey = documentKey;
+ _internalDocument = document;
+ _fromCache = fromCache;
+ }
+ return self;
+}
+
+@dynamic exists;
+
+- (BOOL)exists {
+ return _internalDocument != nil;
+}
+
+- (FIRDocumentReference *)reference {
+ return [FIRDocumentReference referenceWithKey:self.internalKey firestore:self.firestore];
+}
+
+- (NSString *)documentID {
+ return [self.internalKey.path lastSegment];
+}
+
+- (FIRSnapshotMetadata *)metadata {
+ if (!_cachedMetadata) {
+ _cachedMetadata = [FIRSnapshotMetadata
+ snapshotMetadataWithPendingWrites:self.internalDocument.hasLocalMutations
+ fromCache:self.fromCache];
+ }
+ return _cachedMetadata;
+}
+
+- (NSDictionary<NSString *, id> *)data {
+ FSTDocument *document = self.internalDocument;
+
+ if (!document) {
+ FSTThrowInvalidUsage(
+ @"NonExistentDocumentException",
+ @"Document '%@' doesn't exist. "
+ @"Check document.exists to make sure the document exists before calling document.data.",
+ self.internalKey);
+ }
+
+ return [self convertedObject:[self.internalDocument data]];
+}
+
+- (nullable id)objectForKeyedSubscript:(id)key {
+ FIRFieldPath *fieldPath;
+
+ if ([key isKindOfClass:[NSString class]]) {
+ fieldPath = [FIRFieldPath pathWithDotSeparatedString:key];
+ } else if ([key isKindOfClass:[FIRFieldPath class]]) {
+ fieldPath = key;
+ } else {
+ FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath.");
+ }
+
+ FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue];
+ return [self convertedValue:fieldValue];
+}
+
+- (id)convertedValue:(FSTFieldValue *)value {
+ if ([value isKindOfClass:[FSTObjectValue class]]) {
+ return [self convertedObject:(FSTObjectValue *)value];
+ } else if ([value isKindOfClass:[FSTArrayValue class]]) {
+ return [self convertedArray:(FSTArrayValue *)value];
+ } else if ([value isKindOfClass:[FSTReferenceValue class]]) {
+ FSTReferenceValue *ref = (FSTReferenceValue *)value;
+ FSTDatabaseID *refDatabase = ref.databaseID;
+ FSTDatabaseID *database = self.firestore.databaseID;
+ if (![refDatabase isEqualToDatabaseId:database]) {
+ // TODO(b/32073923): Log this as a proper warning.
+ NSLog(
+ @"WARNING: Document %@ contains a document reference within a different database "
+ "(%@/%@) which is not supported. It will be treated as a reference within the "
+ "current database (%@/%@) instead.",
+ self.reference.path, refDatabase.projectID, refDatabase.databaseID, database.projectID,
+ database.databaseID);
+ }
+ return [FIRDocumentReference referenceWithKey:ref.value firestore:self.firestore];
+ } else {
+ return value.value;
+ }
+}
+
+- (NSDictionary<NSString *, id> *)convertedObject:(FSTObjectValue *)objectValue {
+ NSMutableDictionary *result = [NSMutableDictionary dictionary];
+ [objectValue.internalValue
+ enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) {
+ result[key] = [self convertedValue:value];
+ }];
+ return result;
+}
+
+- (NSArray<id> *)convertedArray:(FSTArrayValue *)arrayValue {
+ NSArray<FSTFieldValue *> *internalValue = arrayValue.internalValue;
+ NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count];
+ [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) {
+ [result addObject:[self convertedValue:value]];
+ }];
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFieldPath+Internal.h b/Firestore/Source/API/FIRFieldPath+Internal.h
new file mode 100644
index 0000000..227cdad
--- /dev/null
+++ b/Firestore/Source/API/FIRFieldPath+Internal.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 "FIRFieldPath.h"
+
+@class FSTFieldPath;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRFieldPath ()
+
+- (instancetype)initPrivate:(FSTFieldPath *)path NS_DESIGNATED_INITIALIZER;
+
+/** Internal field path representation */
+@property(nonatomic, strong, readonly) FSTFieldPath *internalValue;
+
+@end
+
+/** Internal FIRFieldPath API we don't want exposed in our public header files. */
+@interface FIRFieldPath (Internal)
+
++ (instancetype)pathWithDotSeparatedString:(NSString *)path;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFieldPath.m b/Firestore/Source/API/FIRFieldPath.m
new file mode 100644
index 0000000..b3c919c
--- /dev/null
+++ b/Firestore/Source/API/FIRFieldPath.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 "FIRFieldPath+Internal.h"
+
+#import "FSTPath.h"
+#import "FSTUsageValidation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRFieldPath
+
+- (instancetype)initWithFields:(NSArray<NSString *> *)fieldNames {
+ if (fieldNames.count == 0) {
+ FSTThrowInvalidArgument(@"Invalid field path. Provided names must not be empty.");
+ }
+
+ for (int i = 0; i < fieldNames.count; ++i) {
+ if (fieldNames[i].length == 0) {
+ FSTThrowInvalidArgument(@"Invalid field name at index %d. Field names must not be empty.", i);
+ }
+ }
+
+ return [self initPrivate:[FSTFieldPath pathWithSegments:fieldNames]];
+}
+
++ (instancetype)documentID {
+ return [[FIRFieldPath alloc] initPrivate:FSTFieldPath.keyFieldPath];
+}
+
+- (instancetype)initPrivate:(FSTFieldPath *)fieldPath {
+ if (self = [super init]) {
+ _internalValue = fieldPath;
+ }
+ return self;
+}
+
++ (instancetype)pathWithDotSeparatedString:(NSString *)path {
+ if ([[FIRFieldPath reservedCharactersRegex]
+ numberOfMatchesInString:path
+ options:0
+ range:NSMakeRange(0, path.length)] > 0) {
+ FSTThrowInvalidArgument(
+ @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", path);
+ }
+ @try {
+ return [[FIRFieldPath alloc] initWithFields:[path componentsSeparatedByString:@"."]];
+ } @catch (NSException *exception) {
+ FSTThrowInvalidArgument(
+ @"Invalid field path (%@). Paths must not be empty, begin with '.', end with '.', or "
+ @"contain '..'",
+ path);
+ }
+}
+
+/** Matches any characters in a field path string that are reserved. */
++ (NSRegularExpression *)reservedCharactersRegex {
+ static NSRegularExpression *regex = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ regex = [NSRegularExpression regularExpressionWithPattern:@"[~*/\\[\\]]" options:0 error:nil];
+ });
+ return regex;
+}
+
+- (id)copyWithZone:(NSZone *__nullable)zone {
+ return [[[self class] alloc] initPrivate:self.internalValue];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+
+ if (![object isKindOfClass:[FIRFieldPath class]]) {
+ return NO;
+ }
+
+ return [self.internalValue isEqual:((FIRFieldPath *)object).internalValue];
+}
+
+- (NSUInteger)hash {
+ return [self.internalValue hash];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFieldValue+Internal.h b/Firestore/Source/API/FIRFieldValue+Internal.h
new file mode 100644
index 0000000..1b4a99c
--- /dev/null
+++ b/Firestore/Source/API/FIRFieldValue+Internal.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 "FIRFieldValue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRFieldValue class for field deletes. Exposed internally so code can do isKindOfClass checks on
+ * it.
+ */
+@interface FSTDeleteFieldValue : FIRFieldValue
+- (instancetype)init NS_UNAVAILABLE;
+@end
+
+/**
+ * FIRFieldValue class for server timestamps. Exposed internally so code can do isKindOfClass checks
+ * on it.
+ */
+@interface FSTServerTimestampFieldValue : FIRFieldValue
+- (instancetype)init NS_UNAVAILABLE;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFieldValue.m b/Firestore/Source/API/FIRFieldValue.m
new file mode 100644
index 0000000..a44d8fa
--- /dev/null
+++ b/Firestore/Source/API/FIRFieldValue.m
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRFieldValue+Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRFieldValue ()
+- (instancetype)initPrivate NS_DESIGNATED_INITIALIZER;
+@end
+
+#pragma mark - FSTDeleteFieldValue
+
+@interface FSTDeleteFieldValue ()
+/** Returns a single shared instance of the class. */
++ (instancetype)deleteFieldValue;
+@end
+
+@implementation FSTDeleteFieldValue
+
+- (instancetype)initPrivate {
+ self = [super initPrivate];
+ return self;
+}
+
++ (instancetype)deleteFieldValue {
+ static FSTDeleteFieldValue *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FSTDeleteFieldValue alloc] initPrivate];
+ });
+ return sharedInstance;
+}
+
+@end
+
+#pragma mark - FSTServerTimestampFieldValue
+
+@interface FSTServerTimestampFieldValue ()
+/** Returns a single shared instance of the class. */
++ (instancetype)serverTimestampFieldValue;
+@end
+
+@implementation FSTServerTimestampFieldValue
+
+- (instancetype)initPrivate {
+ self = [super initPrivate];
+ return self;
+}
+
++ (instancetype)serverTimestampFieldValue {
+ static FSTServerTimestampFieldValue *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FSTServerTimestampFieldValue alloc] initPrivate];
+ });
+ return sharedInstance;
+}
+
+@end
+
+#pragma mark - FIRFieldValue
+
+@implementation FIRFieldValue
+
+- (instancetype)initPrivate {
+ self = [super init];
+ return self;
+}
+
++ (instancetype)fieldValueForDelete {
+ return [FSTDeleteFieldValue deleteFieldValue];
+}
+
++ (instancetype)fieldValueForServerTimestamp {
+ return [FSTServerTimestampFieldValue serverTimestampFieldValue];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFirestore+Internal.h b/Firestore/Source/API/FIRFirestore+Internal.h
new file mode 100644
index 0000000..08f5266
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestore+Internal.h
@@ -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 "FIRFirestore.h"
+#import "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FSTDatabaseID;
+@class FSTDispatchQueue;
+@class FSTFirestoreClient;
+@class FSTUserDataConverter;
+@protocol FSTCredentialsProvider;
+
+@interface FIRFirestore (/* Init */)
+
+/**
+ * Initializes a Firestore object with all the required parameters directly. This exists so that
+ * tests can create FIRFirestore objects without needing FIRApp.
+ */
+- (instancetype)initWithProjectID:(NSString *)projectID
+ database:(NSString *)database
+ persistenceKey:(NSString *)persistenceKey
+ credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ firebaseApp:(FIRApp *)app;
+
+@end
+
+/** Internal FIRFirestore API we don't want exposed in our public header files. */
+@interface FIRFirestore (Internal)
+
+/** Checks to see if logging is is globally enabled for the Firestore client. */
++ (BOOL)isLoggingEnabled;
+
+/**
+ * Shutdown this `FIRFirestore`, releasing all resources (abandoning any outstanding writes,
+ * removing all listens, closing all network connections, etc.).
+ *
+ * @param completion A block to execute once everything has shut down.
+ */
+- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(shutdown(completion:));
+
+@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID;
+@property(nonatomic, strong, readonly) FSTFirestoreClient *client;
+@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFirestore.m b/Firestore/Source/API/FIRFirestore.m
new file mode 100644
index 0000000..e8c4fa6
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestore.m
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRFirestore.h"
+
+#import <FirebaseCommunity/FIRApp.h>
+#import <FirebaseCommunity/FIRLogger.h>
+#import <FirebaseCommunity/FIROptions.h>
+
+#import "FIRCollectionReference+Internal.h"
+#import "FIRDocumentReference+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRFirestoreSettings.h"
+#import "FIRTransaction+Internal.h"
+#import "FIRWriteBatch+Internal.h"
+#import "FSTUserDataConverter.h"
+
+#import "FSTAssert.h"
+#import "FSTCredentialsProvider.h"
+#import "FSTDatabaseID.h"
+#import "FSTDatabaseInfo.h"
+#import "FSTDispatchQueue.h"
+#import "FSTDocumentKey.h"
+#import "FSTFirestoreClient.h"
+#import "FSTLogger.h"
+#import "FSTPath.h"
+#import "FSTUsageValidation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain";
+
+@interface FIRFirestore ()
+
+@property(nonatomic, strong) FSTDatabaseID *databaseID;
+@property(nonatomic, strong) NSString *persistenceKey;
+@property(nonatomic, strong) id<FSTCredentialsProvider> credentialsProvider;
+@property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue;
+
+@property(nonatomic, strong) FSTFirestoreClient *client;
+@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter;
+
+@end
+
+@implementation FIRFirestore {
+ FIRFirestoreSettings *_settings;
+}
+
++ (NSMutableDictionary<NSString *, FIRFirestore *> *)instances {
+ static dispatch_once_t token = 0;
+ static NSMutableDictionary<NSString *, FIRFirestore *> *instances;
+ dispatch_once(&token, ^{
+ instances = [NSMutableDictionary dictionary];
+ });
+ return instances;
+}
+
++ (instancetype)firestore {
+ FIRApp *app = [FIRApp defaultApp];
+ if (!app) {
+ FSTThrowInvalidUsage(
+ @"FIRAppNotConfiguredException",
+ @"Failed to get FIRApp instance. Please call FIRApp.configure() before using FIRFirestore");
+ }
+ return [self firestoreForApp:app database:kDefaultDatabaseID];
+}
+
++ (instancetype)firestoreForApp:(FIRApp *)app {
+ return [self firestoreForApp:app database:kDefaultDatabaseID];
+}
+
+// TODO(b/62410906): make this public
++ (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database {
+ if (!app) {
+ FSTThrowInvalidArgument(
+ @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd "
+ "like to use the default FirebaseApp instance.");
+ }
+ if (!database) {
+ FSTThrowInvalidArgument(
+ @"database identifier may not be nil. Use '%@' if you want the default "
+ "database",
+ kDefaultDatabaseID);
+ }
+ NSString *key = [NSString stringWithFormat:@"%@|%@", app.name, database];
+
+ NSMutableDictionary<NSString *, FIRFirestore *> *instances = self.instances;
+ @synchronized(instances) {
+ FIRFirestore *firestore = instances[key];
+ if (!firestore) {
+ NSString *projectID = app.options.projectID;
+ FSTAssert(projectID, @"FIROptions.projectID cannot be nil.");
+
+ FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue
+ queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)];
+
+ id<FSTCredentialsProvider> credentialsProvider;
+ credentialsProvider = [[FSTFirebaseCredentialsProvider alloc] initWithApp:app];
+
+ NSString *persistenceKey = app.name;
+
+ firestore = [[FIRFirestore alloc] initWithProjectID:projectID
+ database:database
+ persistenceKey:persistenceKey
+ credentialsProvider:credentialsProvider
+ workerDispatchQueue:workerDispatchQueue
+ firebaseApp:app];
+ instances[key] = firestore;
+ }
+
+ return firestore;
+ }
+}
+
+- (instancetype)initWithProjectID:(NSString *)projectID
+ database:(NSString *)database
+ persistenceKey:(NSString *)persistenceKey
+ credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ firebaseApp:(FIRApp *)app {
+ if (self = [super init]) {
+ _databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database];
+ FSTPreConverterBlock block = ^id _Nullable(id _Nullable input) {
+ if ([input isKindOfClass:[FIRDocumentReference class]]) {
+ FIRDocumentReference *documentReference = (FIRDocumentReference *)input;
+ return [[FSTDocumentKeyReference alloc] initWithKey:documentReference.key
+ databaseID:documentReference.firestore.databaseID];
+ } else {
+ return input;
+ }
+ };
+ _dataConverter =
+ [[FSTUserDataConverter alloc] initWithDatabaseID:_databaseID preConverter:block];
+ _persistenceKey = persistenceKey;
+ _credentialsProvider = credentialsProvider;
+ _workerDispatchQueue = workerDispatchQueue;
+ _app = app;
+ _settings = [[FIRFirestoreSettings alloc] init];
+ }
+ return self;
+}
+
+- (FIRFirestoreSettings *)settings {
+ // Disallow mutation of our internal settings
+ return [_settings copy];
+}
+
+- (void)setSettings:(FIRFirestoreSettings *)settings {
+ // As a special exception, don't throw if the same settings are passed repeatedly. This should
+ // make it more friendly to create a Firestore instance.
+ if (_client && ![_settings isEqual:settings]) {
+ FSTThrowInvalidUsage(@"FIRIllegalStateException",
+ @"Firestore instance has already been started and its settings can no "
+ "longer be changed. You can only set settings before calling any "
+ "other methods on a Firestore instance.");
+ }
+ _settings = [settings copy];
+}
+
+/**
+ * Ensures that the FirestoreClient is configured.
+ * @return self
+ */
+- (instancetype)firestoreWithConfiguredClient {
+ if (!_client) {
+ // These values are validated elsewhere; this is just double-checking:
+ FSTAssert(_settings.host, @"FIRFirestoreSettings.host cannot be nil.");
+ FSTAssert(_settings.dispatchQueue, @"FIRFirestoreSettings.dispatchQueue cannot be nil.");
+
+ FSTDatabaseInfo *databaseInfo =
+ [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID
+ persistenceKey:_persistenceKey
+ host:_settings.host
+ sslEnabled:_settings.sslEnabled];
+
+ FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue];
+
+ _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo
+ usePersistence:_settings.persistenceEnabled
+ credentialsProvider:_credentialsProvider
+ userDispatchQueue:userDispatchQueue
+ workerDispatchQueue:_workerDispatchQueue];
+ }
+ return self;
+}
+
+- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath {
+ if (!collectionPath) {
+ FSTThrowInvalidArgument(@"Collection path cannot be nil.");
+ }
+ FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath];
+ return
+ [FIRCollectionReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient];
+}
+
+- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath {
+ if (!documentPath) {
+ FSTThrowInvalidArgument(@"Document path cannot be nil.");
+ }
+ FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath];
+ return [FIRDocumentReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient];
+}
+
+- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock
+ dispatchQueue:(dispatch_queue_t)queue
+ completion:
+ (void (^)(id _Nullable result, NSError *_Nullable error))completion {
+ // We wrap the function they provide in order to use internal implementation classes for
+ // FSTTransaction, and to run the user callback block on the proper queue.
+ if (!updateBlock) {
+ FSTThrowInvalidArgument(@"Transaction block cannot be nil.");
+ } else if (!completion) {
+ FSTThrowInvalidArgument(@"Transaction completion block cannot be nil.");
+ }
+
+ FSTTransactionBlock wrappedUpdate =
+ ^(FSTTransaction *internalTransaction,
+ void (^internalCompletion)(id _Nullable, NSError *_Nullable)) {
+ FIRTransaction *transaction =
+ [FIRTransaction transactionWithFSTTransaction:internalTransaction firestore:self];
+ dispatch_async(queue, ^{
+ NSError *_Nullable error = nil;
+ id _Nullable result = updateBlock(transaction, &error);
+ if (error) {
+ // Force the result to be nil in the case of an error, in case the user set both.
+ result = nil;
+ }
+ internalCompletion(result, error);
+ });
+ };
+ [self firestoreWithConfiguredClient];
+ [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion];
+}
+
+- (FIRWriteBatch *)batch {
+ return [FIRWriteBatch writeBatchWithFirestore:[self firestoreWithConfiguredClient]];
+}
+
+- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock
+ completion:
+ (void (^)(id _Nullable result, NSError *_Nullable error))completion {
+ static dispatch_queue_t transactionDispatchQueue;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ transactionDispatchQueue = dispatch_queue_create("com.google.firebase.firestore.transaction",
+ DISPATCH_QUEUE_CONCURRENT);
+ });
+ [self runTransactionWithBlock:updateBlock
+ dispatchQueue:transactionDispatchQueue
+ completion:completion];
+}
+
+- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion {
+ if (!self.client) {
+ completion(nil);
+ return;
+ }
+ return [self.client shutdownWithCompletion:completion];
+}
+
++ (BOOL)isLoggingEnabled {
+ return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO);
+}
+
++ (void)enableLogging:(BOOL)logging {
+ FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFirestoreSettings.m b/Firestore/Source/API/FIRFirestoreSettings.m
new file mode 100644
index 0000000..106a0b5
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestoreSettings.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 "FIRFirestoreSettings.h"
+
+#import "FSTUsageValidation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const kDefaultHost = @"firestore.googleapis.com";
+static const BOOL kDefaultSSLEnabled = YES;
+static const BOOL kDefaultPersistenceEnabled = YES;
+
+@implementation FIRFirestoreSettings
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _host = kDefaultHost;
+ _sslEnabled = kDefaultSSLEnabled;
+ _dispatchQueue = dispatch_get_main_queue();
+ _persistenceEnabled = kDefaultPersistenceEnabled;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ } else if (![other isKindOfClass:[FIRFirestoreSettings class]]) {
+ return NO;
+ }
+
+ FIRFirestoreSettings *otherSettings = (FIRFirestoreSettings *)other;
+ return [self.host isEqual:otherSettings.host] &&
+ self.isSSLEnabled == otherSettings.isSSLEnabled &&
+ self.dispatchQueue == otherSettings.dispatchQueue &&
+ self.isPersistenceEnabled == otherSettings.isPersistenceEnabled;
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.host hash];
+ result = 31 * result + (self.isSSLEnabled ? 1231 : 1237);
+ // Ignore the dispatchQueue to avoid having to deal with sizeof(dispatch_queue_t).
+ result = 31 * result + (self.isPersistenceEnabled ? 1231 : 1237);
+ return result;
+}
+
+- (id)copyWithZone:(nullable NSZone *)zone {
+ FIRFirestoreSettings *copy = [[FIRFirestoreSettings alloc] init];
+ copy.host = _host;
+ copy.sslEnabled = _sslEnabled;
+ copy.dispatchQueue = _dispatchQueue;
+ copy.persistenceEnabled = _persistenceEnabled;
+ return copy;
+}
+
+- (void)setHost:(NSString *)host {
+ if (!host) {
+ FSTThrowInvalidArgument(
+ @"host setting may not be nil. You should generally just use the default value "
+ "(which is %@)",
+ kDefaultHost);
+ }
+ _host = [host mutableCopy];
+}
+
+- (void)setDispatchQueue:(dispatch_queue_t)dispatchQueue {
+ if (!dispatchQueue) {
+ FSTThrowInvalidArgument(
+ @"dispatch queue setting may not be nil. Create a new dispatch queue with "
+ "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default "
+ "(which is the main queue, returned from dispatch_get_main_queue())");
+ }
+ _dispatchQueue = dispatchQueue;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFirestoreVersion.h b/Firestore/Source/API/FIRFirestoreVersion.h
new file mode 100644
index 0000000..6fb21eb
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestoreVersion.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.
+ */
+
+/** Version for Firestore. */
+
+#import <Foundation/Foundation.h>
+
+/** Version string for the Firebase Firestore SDK. */
+FOUNDATION_EXPORT const unsigned char *const FirebaseFirestoreVersionString;
diff --git a/Firestore/Source/API/FIRFirestoreVersion.m b/Firestore/Source/API/FIRFirestoreVersion.m
new file mode 100644
index 0000000..4f8bb28
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestoreVersion.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.
+ */
+
+#ifndef FIRFirestore_VERSION
+#error "FIRFirestore_VERSION is not defined: add -DFIRFirestore_VERSION=... to the build invocation"
+#endif
+
+// The following two macros supply the incantation so that the C
+// preprocessor does not try to parse the version as a floating
+// point number. See
+// https://www.guyrutenberg.com/2008/12/20/expanding-macros-into-string-constants-in-c/
+#define STR(x) STR_EXPAND(x)
+#define STR_EXPAND(x) #x
+
+const unsigned char *const FirebaseFirestoreVersionString =
+ (const unsigned char *const)STR(FIRFirestore_VERSION);
diff --git a/Firestore/Source/API/FIRGeoPoint+Internal.h b/Firestore/Source/API/FIRGeoPoint+Internal.h
new file mode 100644
index 0000000..6eb8548
--- /dev/null
+++ b/Firestore/Source/API/FIRGeoPoint+Internal.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 "FIRGeoPoint.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRGeoPoint API we don't want exposed in our public header files. */
+@interface FIRGeoPoint (Internal)
+- (NSComparisonResult)compare:(FIRGeoPoint *)other;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRGeoPoint.m b/Firestore/Source/API/FIRGeoPoint.m
new file mode 100644
index 0000000..a50cf37
--- /dev/null
+++ b/Firestore/Source/API/FIRGeoPoint.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 "FIRGeoPoint+Internal.h"
+
+#import "FSTComparison.h"
+#import "FSTUsageValidation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRGeoPoint
+
+- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude {
+ if (self = [super init]) {
+ if (latitude < -90 || latitude > 90 || !isfinite(latitude)) {
+ FSTThrowInvalidArgument(
+ @"GeoPoint requires a latitude value in the range of [-90, 90], "
+ "but was %f",
+ latitude);
+ }
+ if (longitude < -180 || longitude > 180 || !isfinite(longitude)) {
+ FSTThrowInvalidArgument(
+ @"GeoPoint requires a longitude value in the range of [-180, 180], "
+ "but was %f",
+ longitude);
+ }
+
+ _latitude = latitude;
+ _longitude = longitude;
+ }
+ return self;
+}
+
+- (NSComparisonResult)compare:(FIRGeoPoint *)other {
+ NSComparisonResult result = FSTCompareDoubles(self.latitude, other.latitude);
+ if (result != NSOrderedSame) {
+ return result;
+ } else {
+ return FSTCompareDoubles(self.longitude, other.longitude);
+ }
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FIRGeoPoint: (%f, %f)>", self.latitude, self.longitude];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FIRGeoPoint class]]) {
+ return NO;
+ }
+ FIRGeoPoint *otherGeoPoint = (FIRGeoPoint *)other;
+ return FSTDoubleBitwiseEquals(self.latitude, otherGeoPoint.latitude) &&
+ FSTDoubleBitwiseEquals(self.longitude, otherGeoPoint.longitude);
+}
+
+- (NSUInteger)hash {
+ return 31 * FSTDoubleBitwiseHash(self.latitude) + FSTDoubleBitwiseHash(self.longitude);
+}
+
+/** Implements NSCopying without actually copying because geopoints are immutable. */
+- (id)copyWithZone:(NSZone *_Nullable)zone {
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRListenerRegistration+Internal.h b/Firestore/Source/API/FIRListenerRegistration+Internal.h
new file mode 100644
index 0000000..4cd2d57
--- /dev/null
+++ b/Firestore/Source/API/FIRListenerRegistration+Internal.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 "FIRListenerRegistration.h"
+
+@class FSTAsyncQueryListener;
+@class FSTFirestoreClient;
+@class FSTQueryListener;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Private implementation of the FIRListenerRegistration protocol. */
+@interface FSTListenerRegistration : NSObject <FIRListenerRegistration>
+
+- (instancetype)initWithClient:(FSTFirestoreClient *)client
+ asyncListener:(FSTAsyncQueryListener *)asyncListener
+ internalListener:(FSTQueryListener *)internalListener;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRListenerRegistration.m b/Firestore/Source/API/FIRListenerRegistration.m
new file mode 100644
index 0000000..9ce0127
--- /dev/null
+++ b/Firestore/Source/API/FIRListenerRegistration.m
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRListenerRegistration+Internal.h"
+
+#import "FSTAsyncQueryListener.h"
+#import "FSTFirestoreClient.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTListenerRegistration ()
+
+/** The client that was used to register this listen. */
+@property(nonatomic, strong, readonly) FSTFirestoreClient *client;
+
+/** The async listener that is used to mute events synchronously. */
+@property(nonatomic, strong, readonly) FSTAsyncQueryListener *asyncListener;
+
+/** The internal FSTQueryListener that can be used to unlisten the query. */
+@property(nonatomic, strong, readwrite) FSTQueryListener *internalListener;
+
+@end
+
+@implementation FSTListenerRegistration
+
+- (instancetype)initWithClient:(FSTFirestoreClient *)client
+ asyncListener:(FSTAsyncQueryListener *)asyncListener
+ internalListener:(FSTQueryListener *)internalListener {
+ if (self = [super init]) {
+ _client = client;
+ _asyncListener = asyncListener;
+ _internalListener = internalListener;
+ }
+ return self;
+}
+
+- (void)remove {
+ [self.asyncListener mute];
+ [self.client removeListener:self.internalListener];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRQuery+Internal.h b/Firestore/Source/API/FIRQuery+Internal.h
new file mode 100644
index 0000000..3c2b2a7
--- /dev/null
+++ b/Firestore/Source/API/FIRQuery+Internal.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 "FIRQuery.h"
+
+@class FSTQuery;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRQuery API we don't want exposed in our public header files. */
+@interface FIRQuery (Internal)
++ (FIRQuery *)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore;
+@property(nonatomic, strong, readonly) FSTQuery *query;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRQuery.m b/Firestore/Source/API/FIRQuery.m
new file mode 100644
index 0000000..63244fd
--- /dev/null
+++ b/Firestore/Source/API/FIRQuery.m
@@ -0,0 +1,520 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRQuery.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRDocumentReference.h"
+#import "FIRDocumentSnapshot+Internal.h"
+#import "FIRFieldPath+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRListenerRegistration+Internal.h"
+#import "FIRQuery+Internal.h"
+#import "FIRQuerySnapshot+Internal.h"
+#import "FIRQuery_Init.h"
+#import "FIRSnapshotMetadata+Internal.h"
+#import "FSTAssert.h"
+#import "FSTAsyncQueryListener.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTEventManager.h"
+#import "FSTFieldValue.h"
+#import "FSTFirestoreClient.h"
+#import "FSTPath.h"
+#import "FSTQuery.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRQueryListenOptions ()
+
+- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges
+ includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FIRQueryListenOptions
+
++ (instancetype)options {
+ return [[FIRQueryListenOptions alloc] init];
+}
+
+- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges
+ includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges {
+ if (self = [super init]) {
+ _includeQueryMetadataChanges = includeQueryMetadataChanges;
+ _includeDocumentMetadataChanges = includeDocumentMetadataChanges;
+ }
+ return self;
+}
+
+- (instancetype)init {
+ return [self initWithIncludeQueryMetadataChanges:NO includeDocumentMetadataChanges:NO];
+}
+
+- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges {
+ return [[FIRQueryListenOptions alloc]
+ initWithIncludeQueryMetadataChanges:includeQueryMetadataChanges
+ includeDocumentMetadataChanges:_includeDocumentMetadataChanges];
+}
+
+- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges {
+ return [[FIRQueryListenOptions alloc]
+ initWithIncludeQueryMetadataChanges:_includeQueryMetadataChanges
+ includeDocumentMetadataChanges:includeDocumentMetadataChanges];
+}
+
+@end
+
+@interface FIRQuery ()
+@property(nonatomic, strong, readonly) FSTQuery *query;
+@end
+
+@implementation FIRQuery (Internal)
++ (instancetype)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore {
+ return [[FIRQuery alloc] initWithQuery:query firestore:firestore];
+}
+@end
+
+@implementation FIRQuery
+
+#pragma mark - Public Methods
+
+- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore {
+ if (self = [super init]) {
+ _query = query;
+ _firestore = firestore;
+ }
+ return self;
+}
+
+- (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapshot,
+ NSError *_Nullable error))completion {
+ FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES
+ includeDocumentMetadataChanges:YES
+ waitForSyncWhenOnline:YES];
+
+ dispatch_semaphore_t registered = dispatch_semaphore_create(0);
+ __block id<FIRListenerRegistration> listenerRegistration;
+ FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *snapshot, NSError *error) {
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+
+ // Remove query first before passing event to user to avoid user actions affecting the
+ // now stale query.
+ dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER);
+ [listenerRegistration remove];
+
+ completion(snapshot, nil);
+ };
+
+ listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener];
+ dispatch_semaphore_signal(registered);
+}
+
+- (id<FIRListenerRegistration>)addSnapshotListener:(FIRQuerySnapshotBlock)listener {
+ return [self addSnapshotListenerWithOptions:nil listener:listener];
+}
+
+- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions:
+ (nullable FIRQueryListenOptions *)options
+ listener:(FIRQuerySnapshotBlock)listener {
+ return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options]
+ listener:listener];
+}
+
+- (id<FIRListenerRegistration>)
+addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions
+ listener:(FIRQuerySnapshotBlock)listener {
+ FIRFirestore *firestore = self.firestore;
+ FSTQuery *query = self.query;
+
+ FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) {
+ if (error) {
+ listener(nil, error);
+ return;
+ }
+
+ FIRSnapshotMetadata *metadata =
+ [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites
+ fromCache:snapshot.fromCache];
+
+ listener([FIRQuerySnapshot snapshotWithFirestore:firestore
+ originalQuery:query
+ snapshot:snapshot
+ metadata:metadata],
+ nil);
+ };
+
+ FSTAsyncQueryListener *asyncListener =
+ [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue
+ snapshotHandler:snapshotHandler];
+
+ FSTQueryListener *internalListener =
+ [firestore.client listenToQuery:query
+ options:internalOptions
+ viewSnapshotHandler:[asyncListener asyncSnapshotHandler]];
+ return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client
+ asyncListener:asyncListener
+ internalListener:internalListener];
+}
+
+- (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value {
+ return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual field:field value:value];
+}
+
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value {
+ return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual
+ path:path.internalValue
+ value:value];
+}
+
+- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value {
+ return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan field:field value:value];
+}
+
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThan:(id)value {
+ return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan
+ path:path.internalValue
+ value:value];
+}
+
+- (FIRQuery *)queryWhereField:(NSString *)field isLessThanOrEqualTo:(id)value {
+ return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual
+ field:field
+ value:value];
+}
+
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThanOrEqualTo:(id)value {
+ return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual
+ path:path.internalValue
+ value:value];
+}
+
+- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThan:(id)value {
+ return
+ [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan field:field value:value];
+}
+
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThan:(id)value {
+ return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan
+ path:path.internalValue
+ value:value];
+}
+
+- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThanOrEqualTo:(id)value {
+ return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual
+ field:field
+ value:value];
+}
+
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThanOrEqualTo:(id)value {
+ return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual
+ path:path.internalValue
+ value:value];
+}
+
+- (FIRQuery *)queryOrderedByField:(NSString *)field {
+ return
+ [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO];
+}
+
+- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath {
+ return [self queryOrderedByFieldPath:fieldPath descending:NO];
+}
+
+- (FIRQuery *)queryOrderedByField:(NSString *)field descending:(BOOL)descending {
+ return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field]
+ descending:descending];
+}
+
+- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath descending:(BOOL)descending {
+ [self validateNewOrderByPath:fieldPath.internalValue];
+ if (self.query.startAt) {
+ FSTThrowInvalidUsage(
+ @"InvalidQueryException",
+ @"Invalid query. You must not specify a starting point before specifying the order by.");
+ }
+ if (self.query.endAt) {
+ FSTThrowInvalidUsage(
+ @"InvalidQueryException",
+ @"Invalid query. You must not specify an ending point before specifying the order by.");
+ }
+ FSTSortOrder *sortOrder =
+ [FSTSortOrder sortOrderWithFieldPath:fieldPath.internalValue ascending:!descending];
+ return [FIRQuery referenceWithQuery:[self.query queryByAddingSortOrder:sortOrder]
+ firestore:self.firestore];
+}
+
+- (FIRQuery *)queryLimitedTo:(NSInteger)limit {
+ if (limit <= 0) {
+ FSTThrowInvalidArgument(@"Invalid Query. Query limit (%ld) is invalid. Limit must be positive.",
+ (long)limit);
+ }
+ return [FIRQuery referenceWithQuery:[self.query queryBySettingLimit:limit] firestore:_firestore];
+}
+
+- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)snapshot {
+ FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES];
+ return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound]
+ firestore:self.firestore];
+}
+
+- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues {
+ FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES];
+ return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound]
+ firestore:self.firestore];
+}
+
+- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)snapshot {
+ FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO];
+ return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound]
+ firestore:self.firestore];
+}
+
+- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues {
+ FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO];
+ return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound]
+ firestore:self.firestore];
+}
+
+- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)snapshot {
+ FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES];
+ return
+ [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore];
+}
+
+- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues {
+ FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES];
+ return
+ [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore];
+}
+
+- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)snapshot {
+ FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO];
+ return
+ [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore];
+}
+
+- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues {
+ FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO];
+ return
+ [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore];
+}
+
+#pragma mark - Private Methods
+
+/** Private helper for all of the queryWhereField: methods. */
+- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator
+ field:(NSString *)field
+ value:(id)value {
+ return [self queryWithFilterOperator:filterOperator
+ path:[FIRFieldPath pathWithDotSeparatedString:field].internalValue
+ value:value];
+}
+
+- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator
+ path:(FSTFieldPath *)fieldPath
+ value:(id)value {
+ FSTFieldValue *fieldValue;
+ if ([fieldPath isKeyFieldPath]) {
+ if ([value isKindOfClass:[NSString class]]) {
+ NSString *documentKey = (NSString *)value;
+ if ([documentKey containsString:@"/"]) {
+ FSTThrowInvalidArgument(
+ @"Invalid query. When querying by document ID you must provide "
+ "a valid document ID, but '%@' contains a '/' character.",
+ documentKey);
+ } else if (documentKey.length == 0) {
+ FSTThrowInvalidArgument(
+ @"Invalid query. When querying by document ID you must provide "
+ "a valid document ID, but it was an empty string.");
+ }
+ FSTResourcePath *path = [self.query.path pathByAppendingSegment:documentKey];
+ fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithPath:path]
+ databaseID:self.firestore.databaseID];
+ } else if ([value isKindOfClass:[FIRDocumentReference class]]) {
+ FIRDocumentReference *ref = (FIRDocumentReference *)value;
+ fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID];
+ } else {
+ FSTThrowInvalidArgument(
+ @"Invalid query. When querying by document ID you must provide a "
+ "valid string or DocumentReference, but it was of type: %@",
+ NSStringFromClass([value class]));
+ }
+ } else {
+ fieldValue = [self.firestore.dataConverter parsedQueryValue:value];
+ }
+
+ id<FSTFilter> filter;
+ if ([fieldValue isEqual:[FSTNullValue nullValue]]) {
+ if (filterOperator != FSTRelationFilterOperatorEqual) {
+ FSTThrowInvalidUsage(@"InvalidQueryException",
+ @"Invalid Query. You can only perform equality comparisons on nil / "
+ "NSNull.");
+ }
+ filter = [[FSTNullFilter alloc] initWithField:fieldPath];
+ } else if ([fieldValue isEqual:[FSTDoubleValue nanValue]]) {
+ if (filterOperator != FSTRelationFilterOperatorEqual) {
+ FSTThrowInvalidUsage(@"InvalidQueryException",
+ @"Invalid Query. You can only perform equality comparisons on NaN.");
+ }
+ filter = [[FSTNanFilter alloc] initWithField:fieldPath];
+ } else {
+ filter = [FSTRelationFilter filterWithField:fieldPath
+ filterOperator:filterOperator
+ value:fieldValue];
+ [self validateNewRelationFilter:filter];
+ }
+ return [FIRQuery referenceWithQuery:[self.query queryByAddingFilter:filter]
+ firestore:self.firestore];
+}
+
+- (void)validateNewRelationFilter:(FSTRelationFilter *)filter {
+ if ([filter isInequality]) {
+ FSTFieldPath *existingField = [self.query inequalityFilterField];
+ if (existingField && ![existingField isEqual:filter.field]) {
+ FSTThrowInvalidUsage(
+ @"InvalidQueryException",
+ @"Invalid Query. All where filters with an inequality "
+ "(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same "
+ "field. But you have inequality filters on '%@' and '%@'",
+ existingField, filter.field);
+ }
+
+ FSTFieldPath *firstOrderByField = [self.query firstSortOrderField];
+ if (firstOrderByField) {
+ [self validateOrderByField:firstOrderByField matchesInequalityField:filter.field];
+ }
+ }
+}
+
+- (void)validateNewOrderByPath:(FSTFieldPath *)fieldPath {
+ if (![self.query firstSortOrderField]) {
+ // This is the first order by. It must match any inequality.
+ FSTFieldPath *inequalityField = [self.query inequalityFilterField];
+ if (inequalityField) {
+ [self validateOrderByField:fieldPath matchesInequalityField:inequalityField];
+ }
+ }
+}
+
+- (void)validateOrderByField:(FSTFieldPath *)orderByField
+ matchesInequalityField:(FSTFieldPath *)inequalityField {
+ if (!([orderByField isEqual:inequalityField])) {
+ FSTThrowInvalidUsage(
+ @"InvalidQueryException",
+ @"Invalid query. You have a where filter with an "
+ "inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) on field '%@' "
+ "and so you must also use '%@' as your first queryOrderedBy field, but your first "
+ "queryOrderedBy is currently on field '%@' instead.",
+ inequalityField, inequalityField, orderByField);
+ }
+}
+
+/**
+ * Create a FSTBound from a query given the document.
+ *
+ * Note that the FSTBound will always include the key of the document and the position will be
+ * unambiguous.
+ *
+ * Will throw if the document does not contain all fields of the order by of the query.
+ */
+- (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore {
+ if (![snapshot exists]) {
+ FSTThrowInvalidUsage(@"InvalidQueryException",
+ @"Invalid query. You are trying to start or end a query using a document "
+ @"that doesn't exist.");
+ }
+ FSTDocument *document = snapshot.internalDocument;
+ NSMutableArray<FSTFieldValue *> *components = [NSMutableArray array];
+
+ // Because people expect to continue/end a query at the exact document provided, we need to
+ // use the implicit sort order rather than the explicit sort order, because it's guaranteed to
+ // contain the document key. That way the position becomes unambiguous and the query
+ // continues/ends exactly at the provided document. Without the key (by using the explicit sort
+ // orders), multiple documents could match the position, yielding duplicate results.
+ for (FSTSortOrder *sortOrder in self.query.sortOrders) {
+ if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) {
+ [components addObject:[FSTReferenceValue referenceValue:document.key
+ databaseID:self.firestore.databaseID]];
+ } else {
+ FSTFieldValue *value = [document fieldForPath:sortOrder.field];
+ if (value != nil) {
+ [components addObject:value];
+ } else {
+ FSTThrowInvalidUsage(@"InvalidQueryException",
+ @"Invalid query. You are trying to start or end a query using a "
+ "document for which the field '%@' (used as the order by) "
+ "does not exist.",
+ sortOrder.field.canonicalString);
+ }
+ }
+ }
+ return [FSTBound boundWithPosition:components isBefore:isBefore];
+}
+
+/** Converts a list of field values to an FSTBound. */
+- (FSTBound *)boundFromFieldValues:(NSArray<id> *)fieldValues isBefore:(BOOL)isBefore {
+ // Use explicit sort order because it has to match the query the user made
+ NSArray<FSTSortOrder *> *explicitSortOrders = self.query.explicitSortOrders;
+ if (fieldValues.count > explicitSortOrders.count) {
+ FSTThrowInvalidUsage(@"InvalidQueryException",
+ @"Invalid query. You are trying to start or end a query using more values "
+ @"than were specified in the order by.");
+ }
+
+ NSMutableArray<FSTFieldValue *> *components = [NSMutableArray array];
+ [fieldValues enumerateObjectsUsingBlock:^(id rawValue, NSUInteger idx, BOOL *stop) {
+ FSTSortOrder *sortOrder = explicitSortOrders[idx];
+ if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) {
+ if (![rawValue isKindOfClass:[NSString class]]) {
+ FSTThrowInvalidUsage(@"InvalidQueryException",
+ @"Invalid query. Expected a string for the document ID.");
+ }
+ NSString *documentID = (NSString *)rawValue;
+ if ([documentID containsString:@"/"]) {
+ FSTThrowInvalidUsage(@"InvalidQueryException",
+ @"Invalid query. Document ID '%@' contains a slash.", documentID);
+ }
+ FSTDocumentKey *key =
+ [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:documentID]];
+ [components
+ addObject:[FSTReferenceValue referenceValue:key databaseID:self.firestore.databaseID]];
+ } else {
+ FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue];
+ [components addObject:fieldValue];
+ }
+ }];
+
+ return [FSTBound boundWithPosition:components isBefore:isBefore];
+}
+
+/** Converts the public API options object to the internal options object. */
+- (FSTListenOptions *)internalOptions:(nullable FIRQueryListenOptions *)options {
+ return [[FSTListenOptions alloc]
+ initWithIncludeQueryMetadataChanges:options.includeQueryMetadataChanges
+ includeDocumentMetadataChanges:options.includeDocumentMetadataChanges
+ waitForSyncWhenOnline:NO];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRQuerySnapshot+Internal.h b/Firestore/Source/API/FIRQuerySnapshot+Internal.h
new file mode 100644
index 0000000..3a1e9db
--- /dev/null
+++ b/Firestore/Source/API/FIRQuerySnapshot+Internal.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 "FIRQuerySnapshot.h"
+
+@class FIRFirestore;
+@class FIRSnapshotMetadata;
+@class FSTDocumentSet;
+@class FSTQuery;
+@class FSTViewSnapshot;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRQuerySnapshot API we don't want exposed in our public header files. */
+@interface FIRQuerySnapshot (Internal)
+
++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore
+ originalQuery:(FSTQuery *)query
+ snapshot:(FSTViewSnapshot *)snapshot
+ metadata:(FIRSnapshotMetadata *)metadata;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRQuerySnapshot.m b/Firestore/Source/API/FIRQuerySnapshot.m
new file mode 100644
index 0000000..4bf4edf
--- /dev/null
+++ b/Firestore/Source/API/FIRQuerySnapshot.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 "FIRQuerySnapshot+Internal.h"
+
+#import "FIRDocumentChange+Internal.h"
+#import "FIRDocumentSnapshot+Internal.h"
+#import "FIRQuery+Internal.h"
+#import "FIRSnapshotMetadata.h"
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentSet.h"
+#import "FSTQuery.h"
+#import "FSTViewSnapshot.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRQuerySnapshot ()
+
+- (instancetype)initWithFirestore:(FIRFirestore *)firestore
+ originalQuery:(FSTQuery *)query
+ snapshot:(FSTViewSnapshot *)snapshot
+ metadata:(FIRSnapshotMetadata *)metadata;
+
+@property(nonatomic, strong, readonly) FIRFirestore *firestore;
+@property(nonatomic, strong, readonly) FSTQuery *originalQuery;
+@property(nonatomic, strong, readonly) FSTViewSnapshot *snapshot;
+
+@end
+
+@implementation FIRQuerySnapshot (Internal)
+
++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore
+ originalQuery:(FSTQuery *)query
+ snapshot:(FSTViewSnapshot *)snapshot
+ metadata:(FIRSnapshotMetadata *)metadata {
+ return [[FIRQuerySnapshot alloc] initWithFirestore:firestore
+ originalQuery:query
+ snapshot:snapshot
+ metadata:metadata];
+}
+
+@end
+
+@implementation FIRQuerySnapshot {
+ // Cached value of the documents property.
+ NSArray<FIRDocumentSnapshot *> *_documents;
+
+ // Cached value of the documentChanges property.
+ NSArray<FIRDocumentChange *> *_documentChanges;
+}
+
+- (instancetype)initWithFirestore:(FIRFirestore *)firestore
+ originalQuery:(FSTQuery *)query
+ snapshot:(FSTViewSnapshot *)snapshot
+ metadata:(FIRSnapshotMetadata *)metadata {
+ if (self = [super init]) {
+ _firestore = firestore;
+ _originalQuery = query;
+ _snapshot = snapshot;
+ _metadata = metadata;
+ }
+ return self;
+}
+
+@dynamic empty;
+
+- (FIRQuery *)query {
+ return [FIRQuery referenceWithQuery:self.originalQuery firestore:self.firestore];
+}
+
+- (BOOL)isEmpty {
+ return self.snapshot.documents.isEmpty;
+}
+
+// This property is exposed as an NSInteger instead of an NSUInteger since (as of Xcode 8.1)
+// Swift bridges NSUInteger as UInt, and we want to avoid forcing Swift users to cast their ints
+// where we can. See cr/146959032 for additional context.
+- (NSInteger)count {
+ return self.snapshot.documents.count;
+}
+
+- (NSArray<FIRDocumentSnapshot *> *)documents {
+ if (!_documents) {
+ FSTDocumentSet *documentSet = self.snapshot.documents;
+ FIRFirestore *firestore = self.firestore;
+ BOOL fromCache = self.metadata.fromCache;
+
+ NSMutableArray<FIRDocumentSnapshot *> *result = [NSMutableArray array];
+ for (FSTDocument *document in documentSet.documentEnumerator) {
+ [result addObject:[FIRDocumentSnapshot snapshotWithFirestore:firestore
+ documentKey:document.key
+ document:document
+ fromCache:fromCache]];
+ }
+
+ _documents = result;
+ }
+ return _documents;
+}
+
+- (NSArray<FIRDocumentChange *> *)documentChanges {
+ if (!_documentChanges) {
+ _documentChanges =
+ [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore];
+ }
+ return _documentChanges;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRQuery_Init.h b/Firestore/Source/API/FIRQuery_Init.h
new file mode 100644
index 0000000..d6b0f37
--- /dev/null
+++ b/Firestore/Source/API/FIRQuery_Init.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 "FIRQuery.h"
+
+@class FSTQuery;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An Internal class extension for `FIRQuery` that exposes the init method to classes
+ * that need to derive from it.
+ */
+@interface FIRQuery (/*Init*/)
+- (instancetype)initWithQuery:(FSTQuery *)query
+ firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRSetOptions+Internal.h b/Firestore/Source/API/FIRSetOptions+Internal.h
new file mode 100644
index 0000000..9118096
--- /dev/null
+++ b/Firestore/Source/API/FIRSetOptions+Internal.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 "FIRSetOptions.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSetOptions ()
+
+- (instancetype)initWithMerge:(BOOL)merge NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface FIRSetOptions (Internal)
+
++ (instancetype)overwrite;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRSetOptions.m b/Firestore/Source/API/FIRSetOptions.m
new file mode 100644
index 0000000..ea68c63
--- /dev/null
+++ b/Firestore/Source/API/FIRSetOptions.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 "FIRSetOptions+Internal.h"
+#import "FSTMutation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRSetOptions
+
+- (instancetype)initWithMerge:(BOOL)merge {
+ if (self = [super init]) {
+ _merge = merge;
+ }
+ return self;
+}
+
++ (instancetype)merge {
+ return [[FIRSetOptions alloc] initWithMerge:YES];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ } else if (![other isKindOfClass:[FIRSetOptions class]]) {
+ return NO;
+ }
+
+ FIRSetOptions *otherOptions = (FIRSetOptions *)other;
+
+ return otherOptions.merge != self.merge;
+}
+
+- (NSUInteger)hash {
+ return self.merge ? 1231 : 1237;
+}
+@end
+
+@implementation FIRSetOptions (Internal)
+
++ (instancetype)overwrite {
+ static FIRSetOptions *overwriteInstance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ overwriteInstance = [[FIRSetOptions alloc] initWithMerge:NO];
+ });
+ return overwriteInstance;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRSnapshotMetadata+Internal.h b/Firestore/Source/API/FIRSnapshotMetadata+Internal.h
new file mode 100644
index 0000000..d3265cd
--- /dev/null
+++ b/Firestore/Source/API/FIRSnapshotMetadata+Internal.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 "FIRSnapshotMetadata.h"
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSnapshotMetadata (Internal)
+
++ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRSnapshotMetadata.m b/Firestore/Source/API/FIRSnapshotMetadata.m
new file mode 100644
index 0000000..ff49d8f
--- /dev/null
+++ b/Firestore/Source/API/FIRSnapshotMetadata.m
@@ -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 "FIRSnapshotMetadata.h"
+
+#import "FIRSnapshotMetadata+Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSnapshotMetadata ()
+
+- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache;
+
+@end
+
+@implementation FIRSnapshotMetadata (Internal)
+
++ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache {
+ return [[FIRSnapshotMetadata alloc] initWithPendingWrites:pendingWrites fromCache:fromCache];
+}
+
+@end
+
+@implementation FIRSnapshotMetadata
+
+- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache {
+ if (self = [super init]) {
+ _pendingWrites = pendingWrites;
+ _fromCache = fromCache;
+ }
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRTransaction+Internal.h b/Firestore/Source/API/FIRTransaction+Internal.h
new file mode 100644
index 0000000..8fd3f65
--- /dev/null
+++ b/Firestore/Source/API/FIRTransaction+Internal.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 "FIRTransaction.h"
+
+@class FIRFirestore;
+@class FSTTransaction;
+
+@interface FIRTransaction (Internal)
+
++ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction
+ firestore:(FIRFirestore *)firestore;
+
+@end
diff --git a/Firestore/Source/API/FIRTransaction.m b/Firestore/Source/API/FIRTransaction.m
new file mode 100644
index 0000000..02006bb
--- /dev/null
+++ b/Firestore/Source/API/FIRTransaction.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 "FIRTransaction+Internal.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRDocumentSnapshot+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRSetOptions+Internal.h"
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTTransaction.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FIRTransaction
+
+@interface FIRTransaction ()
+
+- (instancetype)initWithTransaction:(FSTTransaction *)transaction
+ firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FSTTransaction *internalTransaction;
+@property(nonatomic, strong, readonly) FIRFirestore *firestore;
+@end
+
+@implementation FIRTransaction (Internal)
+
++ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction
+ firestore:(FIRFirestore *)firestore {
+ return [[FIRTransaction alloc] initWithTransaction:transaction firestore:firestore];
+}
+
+@end
+
+@implementation FIRTransaction
+
+- (instancetype)initWithTransaction:(FSTTransaction *)transaction
+ firestore:(FIRFirestore *)firestore {
+ self = [super init];
+ if (self) {
+ _internalTransaction = transaction;
+ _firestore = firestore;
+ }
+ return self;
+}
+
+- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document {
+ return [self setData:data forDocument:document options:[FIRSetOptions overwrite]];
+}
+
+- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document
+ options:(FIRSetOptions *)options {
+ [self validateReference:document];
+ FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options];
+ [self.internalTransaction setData:parsed forDocument:document.key];
+ return self;
+}
+
+- (FIRTransaction *)updateData:(NSDictionary<id, id> *)fields
+ forDocument:(FIRDocumentReference *)document {
+ [self validateReference:document];
+ FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields];
+ [self.internalTransaction updateData:parsed forDocument:document.key];
+ return self;
+}
+
+- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document {
+ [self validateReference:document];
+ [self.internalTransaction deleteDocument:document.key];
+ return self;
+}
+
+- (void)getDocument:(FIRDocumentReference *)document
+ completion:(void (^)(FIRDocumentSnapshot *_Nullable document,
+ NSError *_Nullable error))completion {
+ [self validateReference:document];
+ [self.internalTransaction
+ lookupDocumentsForKeys:@[ document.key ]
+ completion:^(NSArray<FSTMaybeDocument *> *_Nullable documents,
+ NSError *_Nullable error) {
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+ FSTAssert(documents.count == 1,
+ @"Mismatch in docs returned from document lookup.");
+ FSTMaybeDocument *internalDoc = documents.firstObject;
+ if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) {
+ completion(nil, nil);
+ return;
+ }
+ FIRDocumentSnapshot *doc =
+ [FIRDocumentSnapshot snapshotWithFirestore:self.firestore
+ documentKey:internalDoc.key
+ document:(FSTDocument *)internalDoc
+ fromCache:NO];
+ completion(doc, nil);
+ }];
+}
+
+- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document
+ error:(NSError *__autoreleasing *)error {
+ [self validateReference:document];
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
+ __block FIRDocumentSnapshot *result;
+ // We have to explicitly assign the innerError into a local to cause it to retain correctly.
+ __block NSError *outerError = nil;
+ [self getDocument:document
+ completion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable innerError) {
+ result = snapshot;
+ outerError = innerError;
+ dispatch_semaphore_signal(semaphore);
+ }];
+ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
+ if (error) {
+ *error = outerError;
+ }
+ return result;
+}
+
+- (void)validateReference:(FIRDocumentReference *)reference {
+ if (reference.firestore != self.firestore) {
+ FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance.");
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRWriteBatch+Internal.h b/Firestore/Source/API/FIRWriteBatch+Internal.h
new file mode 100644
index 0000000..a434e02
--- /dev/null
+++ b/Firestore/Source/API/FIRWriteBatch+Internal.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 "FIRWriteBatch.h"
+
+@class FIRFirestore;
+
+@interface FIRWriteBatch (Internal)
+
++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore;
+
+@end
diff --git a/Firestore/Source/API/FIRWriteBatch.m b/Firestore/Source/API/FIRWriteBatch.m
new file mode 100644
index 0000000..32b6ce8
--- /dev/null
+++ b/Firestore/Source/API/FIRWriteBatch.m
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRWriteBatch+Internal.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRSetOptions+Internal.h"
+#import "FSTFirestoreClient.h"
+#import "FSTMutation.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FIRWriteBatch
+
+@interface FIRWriteBatch ()
+
+- (instancetype)initWithFirestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FIRFirestore *firestore;
+@property(nonatomic, strong, readonly) NSMutableArray<FSTMutation *> *mutations;
+@property(nonatomic, assign) BOOL committed;
+
+@end
+
+@implementation FIRWriteBatch (Internal)
+
++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore {
+ return [[FIRWriteBatch alloc] initWithFirestore:firestore];
+}
+
+@end
+
+@implementation FIRWriteBatch
+
+- (instancetype)initWithFirestore:(FIRFirestore *)firestore {
+ self = [super init];
+ if (self) {
+ _firestore = firestore;
+ _mutations = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document {
+ return [self setData:data forDocument:document options:[FIRSetOptions overwrite]];
+}
+
+- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document
+ options:(FIRSetOptions *)options {
+ [self verifyNotCommitted];
+ [self validateReference:document];
+ FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options];
+ [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key
+ precondition:[FSTPrecondition none]]];
+ return self;
+}
+
+- (FIRWriteBatch *)updateData:(NSDictionary<id, id> *)fields
+ forDocument:(FIRDocumentReference *)document {
+ [self verifyNotCommitted];
+ [self validateReference:document];
+ FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields];
+ [self.mutations
+ addObjectsFromArray:[parsed mutationsWithKey:document.key
+ precondition:[FSTPrecondition preconditionWithExists:YES]]];
+ return self;
+}
+
+- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document {
+ [self verifyNotCommitted];
+ [self validateReference:document];
+ [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key
+ precondition:[FSTPrecondition none]]];
+ return self;
+}
+
+- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion {
+ [self verifyNotCommitted];
+ self.committed = TRUE;
+ [self.firestore.client writeMutations:self.mutations completion:completion];
+}
+
+- (void)verifyNotCommitted {
+ if (self.committed) {
+ FSTThrowInvalidUsage(@"FIRIllegalStateException",
+ @"A write batch can no longer be used after commit has been called.");
+ }
+}
+
+- (void)validateReference:(FIRDocumentReference *)reference {
+ if (reference.firestore != self.firestore) {
+ FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance.");
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FSTUserDataConverter.h b/Firestore/Source/API/FSTUserDataConverter.h
new file mode 100644
index 0000000..69d1fa9
--- /dev/null
+++ b/Firestore/Source/API/FSTUserDataConverter.h
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 FIRSetOptions;
+@class FSTDatabaseID;
+@class FSTDocumentKey;
+@class FSTObjectValue;
+@class FSTFieldMask;
+@class FSTFieldValue;
+@class FSTFieldTransform;
+@class FSTMutation;
+@class FSTPrecondition;
+@class FSTSnapshotVersion;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** The result of parsing document data (e.g. for a setData call). */
+@interface FSTParsedSetData : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (instancetype)initWithData:(FSTObjectValue *)data
+ fieldMask:(nullable FSTFieldMask *)fieldMask
+ fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms
+ NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FSTObjectValue *data;
+@property(nonatomic, strong, readonly, nullable) FSTFieldMask *fieldMask;
+@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms;
+
+/**
+ * Converts the parsed document data into 1 or 2 mutations (depending on whether there are any
+ * field transforms) using the specified document key and precondition.
+ */
+- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition;
+
+@end
+
+/** The result of parsing "update" data (i.e. for an updateData call). */
+@interface FSTParsedUpdateData : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (instancetype)initWithData:(FSTObjectValue *)data
+ fieldMask:(FSTFieldMask *)fieldMask
+ fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms
+ NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FSTObjectValue *data;
+@property(nonatomic, strong, readonly) FSTFieldMask *fieldMask;
+@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms;
+
+/**
+ * Converts the parsed update data into 1 or 2 mutations (depending on whether there are any
+ * field transforms) using the specified document key and precondition.
+ */
+- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition;
+
+@end
+
+/**
+ * An internal representation of FIRDocumentReference, representing a key in a specific database.
+ * This is necessary because keys assume a database from context (usually the current one).
+ * FSTDocumentKeyReference binds a key to a specific databaseID.
+ *
+ * TODO(b/64160088): Make FSTDocumentKey aware of the specific databaseID it is tied to.
+ */
+@interface FSTDocumentKeyReference : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ databaseID:(FSTDatabaseID *)databaseID NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID;
+
+@end
+
+/**
+ * An interface that allows arbitrary pre-converting of user data.
+ *
+ * Returns the converted value (can return back the input to act as a no-op).
+ */
+typedef id _Nullable (^FSTPreConverterBlock)(id _Nullable);
+
+/**
+ * Helper for parsing raw user input (provided via the API) into internal model classes.
+ */
+@interface FSTUserDataConverter : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID
+ preConverter:(FSTPreConverterBlock)preConverter NS_DESIGNATED_INITIALIZER;
+
+/** Parse document data (e.g. from a setData call). */
+- (FSTParsedSetData *)parsedSetData:(id)input options:(FIRSetOptions *)options;
+
+/** Parse "update" data (i.e. from an updateData call). */
+- (FSTParsedUpdateData *)parsedUpdateData:(id)input;
+
+/** Parse a "query value" (e.g. value in a where filter or a value in a cursor bound). */
+- (FSTFieldValue *)parsedQueryValue:(id)input;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FSTUserDataConverter.m b/Firestore/Source/API/FSTUserDataConverter.m
new file mode 100644
index 0000000..7a6c950
--- /dev/null
+++ b/Firestore/Source/API/FSTUserDataConverter.m
@@ -0,0 +1,568 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTUserDataConverter.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRFieldPath+Internal.h"
+#import "FIRFieldValue+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRGeoPoint.h"
+#import "FIRSetOptions+Internal.h"
+#import "FSTAssert.h"
+#import "FSTDatabaseID.h"
+#import "FSTDocumentKey.h"
+#import "FSTFieldValue.h"
+#import "FSTMutation.h"
+#import "FSTPath.h"
+#import "FSTTimestamp.h"
+#import "FSTUsageValidation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const RESERVED_FIELD_DESIGNATOR = @"__";
+
+#pragma mark - FSTParsedSetData
+
+@implementation FSTParsedSetData
+- (instancetype)initWithData:(FSTObjectValue *)data
+ fieldMask:(nullable FSTFieldMask *)fieldMask
+ fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms {
+ self = [super init];
+ if (self) {
+ _data = data;
+ _fieldMask = fieldMask;
+ _fieldTransforms = fieldTransforms;
+ }
+ return self;
+}
+
+- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition {
+ NSMutableArray<FSTMutation *> *mutations = [NSMutableArray array];
+ if (self.fieldMask) {
+ [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key
+ fieldMask:self.fieldMask
+ value:self.data
+ precondition:precondition]];
+ } else {
+ [mutations addObject:[[FSTSetMutation alloc] initWithKey:key
+ value:self.data
+ precondition:precondition]];
+ }
+ if (self.fieldTransforms.count > 0) {
+ [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key
+ fieldTransforms:self.fieldTransforms]];
+ }
+ return mutations;
+}
+
+@end
+
+#pragma mark - FSTParsedUpdateData
+
+@implementation FSTParsedUpdateData
+- (instancetype)initWithData:(FSTObjectValue *)data
+ fieldMask:(FSTFieldMask *)fieldMask
+ fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms {
+ self = [super init];
+ if (self) {
+ _data = data;
+ _fieldMask = fieldMask;
+ _fieldTransforms = fieldTransforms;
+ }
+ return self;
+}
+
+- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition {
+ NSMutableArray<FSTMutation *> *mutations = [NSMutableArray array];
+ [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key
+ fieldMask:self.fieldMask
+ value:self.data
+ precondition:precondition]];
+ if (self.fieldTransforms.count > 0) {
+ [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key
+ fieldTransforms:self.fieldTransforms]];
+ }
+ return mutations;
+}
+
+@end
+
+/**
+ * Represents what type of API method provided the data being parsed; useful for determining which
+ * error conditions apply during parsing and providing better error messages.
+ */
+typedef NS_ENUM(NSInteger, FSTUserDataSource) {
+ FSTUserDataSourceSet,
+ FSTUserDataSourceUpdate,
+ FSTUserDataSourceQueryValue, // from a where clause or cursor bound.
+};
+
+#pragma mark - FSTParseContext
+
+/**
+ * A "context" object passed around while parsing user data.
+ */
+@interface FSTParseContext : NSObject
+/** The current path being parsed. */
+// TODO(b/34871131): path should never be nil, but we don't support array paths right now.
+@property(strong, nonatomic, readonly, nullable) FSTFieldPath *path;
+
+/**
+ * What type of API method provided the data being parsed; useful for determining which error
+ * conditions apply during parsing and providing better error messages.
+ */
+@property(nonatomic, assign) FSTUserDataSource dataSource;
+@property(nonatomic, strong, readonly) NSMutableArray<FSTFieldTransform *> *fieldTransforms;
+@property(nonatomic, strong, readonly) NSMutableArray<FSTFieldPath *> *fieldMask;
+
+- (instancetype)init NS_UNAVAILABLE;
+/**
+ * Initializes a FSTParseContext with the given source and path.
+ *
+ * @param dataSource Indicates what kind of API method this data came from.
+ * @param path A path within the object being parsed. This could be an empty path (in which case
+ * the context represents the root of the data being parsed), or a nonempty path (indicating the
+ * context represents a nested location within the data).
+ *
+ * TODO(b/34871131): We don't support array paths right now, so path can be nil to indicate
+ * the context represents any location within an array (in which case certain features will not work
+ * and errors will be somewhat compromised).
+ */
+- (instancetype)initWithSource:(FSTUserDataSource)dataSource
+ path:(nullable FSTFieldPath *)path
+ fieldTransforms:(NSMutableArray<FSTFieldTransform *> *)fieldTransforms
+ fieldMask:(NSMutableArray<FSTFieldPath *> *)fieldMask
+ NS_DESIGNATED_INITIALIZER;
+
+// Helpers to get a FSTParseContext for a child field.
+- (instancetype)contextForField:(NSString *)fieldName;
+- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath;
+- (instancetype)contextForArrayIndex:(NSUInteger)index;
+@end
+
+@implementation FSTParseContext
+
++ (instancetype)contextWithSource:(FSTUserDataSource)dataSource path:(nullable FSTFieldPath *)path {
+ FSTParseContext *context = [[FSTParseContext alloc] initWithSource:dataSource
+ path:path
+ fieldTransforms:[NSMutableArray array]
+ fieldMask:[NSMutableArray array]];
+ [context validatePath];
+ return context;
+}
+
+- (instancetype)initWithSource:(FSTUserDataSource)dataSource
+ path:(nullable FSTFieldPath *)path
+ fieldTransforms:(NSMutableArray<FSTFieldTransform *> *)fieldTransforms
+ fieldMask:(NSMutableArray<FSTFieldPath *> *)fieldMask {
+ if (self = [super init]) {
+ _dataSource = dataSource;
+ _path = path;
+ _fieldTransforms = fieldTransforms;
+ _fieldMask = fieldMask;
+ }
+ return self;
+}
+
+- (instancetype)contextForField:(NSString *)fieldName {
+ FSTParseContext *context =
+ [[FSTParseContext alloc] initWithSource:self.dataSource
+ path:[self.path pathByAppendingSegment:fieldName]
+ fieldTransforms:self.fieldTransforms
+ fieldMask:self.fieldMask];
+ [context validatePathSegment:fieldName];
+ return context;
+}
+
+- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath {
+ FSTParseContext *context =
+ [[FSTParseContext alloc] initWithSource:self.dataSource
+ path:[self.path pathByAppendingPath:fieldPath]
+ fieldTransforms:self.fieldTransforms
+ fieldMask:self.fieldMask];
+ [context validatePath];
+ return context;
+}
+
+- (instancetype)contextForArrayIndex:(NSUInteger)index {
+ // TODO(b/34871131): We don't support array paths right now; so make path nil.
+ return [[FSTParseContext alloc] initWithSource:self.dataSource
+ path:nil
+ fieldTransforms:self.fieldTransforms
+ fieldMask:self.fieldMask];
+}
+
+/**
+ * Returns a string that can be appended to error messages indicating what field caused the error.
+ */
+- (NSString *)fieldDescription {
+ // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays.
+ if (!self.path || self.path.empty) {
+ return @"";
+ } else {
+ return [NSString stringWithFormat:@" (found in field %@)", self.path];
+ }
+}
+
+- (BOOL)isWrite {
+ return _dataSource == FSTUserDataSourceSet || _dataSource == FSTUserDataSourceUpdate;
+}
+
+- (void)validatePath {
+ // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays.
+ if (self.path == nil) {
+ return;
+ }
+ for (int i = 0; i < self.path.length; i++) {
+ [self validatePathSegment:[self.path segmentAtIndex:i]];
+ }
+}
+
+- (void)validatePathSegment:(NSString *)segment {
+ if ([self isWrite] && [segment hasPrefix:RESERVED_FIELD_DESIGNATOR] &&
+ [segment hasSuffix:RESERVED_FIELD_DESIGNATOR]) {
+ FSTThrowInvalidArgument(@"Document fields cannot begin and end with %@%@",
+ RESERVED_FIELD_DESIGNATOR, [self fieldDescription]);
+ }
+}
+
+@end
+
+#pragma mark - FSTDocumentKeyReference
+
+@implementation FSTDocumentKeyReference
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key databaseID:(FSTDatabaseID *)databaseID {
+ self = [super init];
+ if (self) {
+ _key = key;
+ _databaseID = databaseID;
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - FSTUserDataConverter
+
+@interface FSTUserDataConverter ()
+@property(strong, nonatomic, readonly) FSTDatabaseID *databaseID;
+@property(strong, nonatomic, readonly) FSTPreConverterBlock preConverter;
+@end
+
+@implementation FSTUserDataConverter
+
+- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID
+ preConverter:(FSTPreConverterBlock)preConverter {
+ self = [super init];
+ if (self) {
+ _databaseID = databaseID;
+ _preConverter = preConverter;
+ }
+ return self;
+}
+
+- (FSTParsedSetData *)parsedSetData:(id)input options:(FIRSetOptions *)options {
+ // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust
+ // Obj-C to verify the type for us.
+ if (![input isKindOfClass:[NSDictionary class]]) {
+ FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary.");
+ }
+
+ FSTParseContext *context =
+ [FSTParseContext contextWithSource:FSTUserDataSourceSet path:[FSTFieldPath emptyPath]];
+
+ __block FSTObjectValue *updateData = [FSTObjectValue objectValue];
+
+ [input enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+ // Treat key as a complete field name (don't split on dots, etc.)
+ FSTFieldPath *path = [[FIRFieldPath alloc] initWithFields:@[ key ]].internalValue;
+
+ value = self.preConverter(value);
+
+ FSTFieldValue *_Nullable parsedValue =
+ [self parseData:value context:[context contextForFieldPath:path]];
+ if (parsedValue) {
+ updateData = [updateData objectBySettingValue:parsedValue forPath:path];
+ }
+ }];
+
+ return [[FSTParsedSetData alloc]
+ initWithData:updateData
+ fieldMask:options.merge ? [[FSTFieldMask alloc] initWithFields:context.fieldMask] : nil
+ fieldTransforms:context.fieldTransforms];
+}
+
+- (FSTParsedUpdateData *)parsedUpdateData:(id)input {
+ // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust
+ // Obj-C to verify the type for us.
+ if (![input isKindOfClass:[NSDictionary class]]) {
+ FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary.");
+ }
+
+ NSDictionary *dict = input;
+
+ NSMutableArray<FSTFieldPath *> *fieldMaskPaths = [NSMutableArray array];
+ __block FSTObjectValue *updateData = [FSTObjectValue objectValue];
+
+ FSTParseContext *context =
+ [FSTParseContext contextWithSource:FSTUserDataSourceUpdate path:[FSTFieldPath emptyPath]];
+ [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ FSTFieldPath *path;
+
+ if ([key isKindOfClass:[NSString class]]) {
+ path = [FIRFieldPath pathWithDotSeparatedString:key].internalValue;
+ } else if ([key isKindOfClass:[FIRFieldPath class]]) {
+ path = ((FIRFieldPath *)key).internalValue;
+ } else {
+ FSTThrowInvalidArgument(
+ @"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths.");
+ }
+
+ value = self.preConverter(value);
+ if ([value isKindOfClass:[FSTDeleteFieldValue class]]) {
+ // Add it to the field mask, but don't add anything to updateData.
+ [fieldMaskPaths addObject:path];
+ } else {
+ FSTFieldValue *_Nullable parsedValue =
+ [self parseData:value context:[context contextForFieldPath:path]];
+ if (parsedValue) {
+ [fieldMaskPaths addObject:path];
+ updateData = [updateData objectBySettingValue:parsedValue forPath:path];
+ }
+ }
+ }];
+
+ FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:fieldMaskPaths];
+ return [[FSTParsedUpdateData alloc] initWithData:updateData
+ fieldMask:mask
+ fieldTransforms:context.fieldTransforms];
+}
+
+- (FSTFieldValue *)parsedQueryValue:(id)input {
+ FSTParseContext *context =
+ [FSTParseContext contextWithSource:FSTUserDataSourceQueryValue path:[FSTFieldPath emptyPath]];
+ FSTFieldValue *_Nullable parsed = [self parseData:input context:context];
+ FSTAssert(parsed, @"Parsed data should not be nil.");
+ FSTAssert(context.fieldTransforms.count == 0, @"Field transforms should have been disallowed.");
+ return parsed;
+}
+
+/**
+ * Internal helper for parsing user data.
+ *
+ * @param input Data to be parsed.
+ * @param context A context object representing the current path being parsed, the source of the
+ * data being parsed, etc.
+ *
+ * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be
+ * included in the resulting parsed data.
+ */
+- (nullable FSTFieldValue *)parseData:(id)input context:(FSTParseContext *)context {
+ input = self.preConverter(input);
+ if ([input isKindOfClass:[NSArray class]]) {
+ // TODO(b/34871131): We may need a different way to detect nested arrays once we support array
+ // paths (at which point we should include the path containing the array in the error message).
+ if (!context.path) {
+ FSTThrowInvalidArgument(@"Nested arrays are not supported");
+ }
+ NSArray *array = input;
+ NSMutableArray<FSTFieldValue *> *result = [NSMutableArray arrayWithCapacity:array.count];
+ [array enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *stop) {
+ FSTFieldValue *_Nullable parsedEntry =
+ [self parseData:entry context:[context contextForArrayIndex:idx]];
+ if (!parsedEntry) {
+ // Just include nulls in the array for fields being replaced with a sentinel.
+ parsedEntry = [FSTNullValue nullValue];
+ }
+ [result addObject:parsedEntry];
+ }];
+ // We don't support field mask paths more granular than the top-level array.
+ [context.fieldMask addObject:context.path];
+ return [[FSTArrayValue alloc] initWithValueNoCopy:result];
+
+ } else if ([input isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *dict = input;
+ NSMutableDictionary<NSString *, FSTFieldValue *> *result =
+ [NSMutableDictionary dictionaryWithCapacity:dict.count];
+ [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+ FSTFieldValue *_Nullable parsedValue =
+ [self parseData:value context:[context contextForField:key]];
+ if (parsedValue) {
+ result[key] = parsedValue;
+ }
+ }];
+ return [[FSTObjectValue alloc] initWithDictionary:result];
+
+ } else {
+ // If context.path is null, we are inside an array and we should have already added the root of
+ // the array to the field mask.
+ if (context.path) {
+ [context.fieldMask addObject:context.path];
+ }
+ return [self parseScalarValue:input context:context];
+ }
+}
+
+/**
+ * Helper to parse a scalar value (i.e. not an NSDictionary or NSArray).
+ *
+ * Note that it handles all NSNumber values that are encodable as int64_t or doubles
+ * (depending on the underlying type of the NSNumber). Unsigned integer values are handled though
+ * any value outside what is representable by int64_t (a signed 64-bit value) will throw an
+ * exception.
+ *
+ * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be
+ * included in the resulting parsed data.
+ */
+- (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTParseContext *)context {
+ if (!input || [input isMemberOfClass:[NSNull class]]) {
+ return [FSTNullValue nullValue];
+
+ } else if ([input isKindOfClass:[NSNumber class]]) {
+ // Recover the underlying type of the number, using the method described here:
+ // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber
+ const char *cType = [input objCType];
+
+ // Type Encoding values taken from
+ // https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/
+ // Articles/ocrtTypeEncodings.html
+ switch (cType[0]) {
+ case 'q':
+ return [FSTIntegerValue integerValue:[input longLongValue]];
+
+ case 'i': // Falls through.
+ case 's': // Falls through.
+ case 'l': // Falls through.
+ case 'I': // Falls through.
+ case 'S':
+ // Coerce integer values that aren't long long. Allow unsigned integer types that are
+ // guaranteed small enough to skip a length check.
+ return [FSTIntegerValue integerValue:[input longLongValue]];
+
+ case 'L': // Falls through.
+ case 'Q':
+ // Unsigned integers that could be too large. Note that the 'L' (long) case is handled here
+ // because when compiled for LP64, unsigned long is 64 bits and could overflow int64_t.
+ {
+ unsigned long long extended = [input unsignedLongLongValue];
+
+ if (extended > LLONG_MAX) {
+ FSTThrowInvalidArgument(@"NSNumber (%llu) is too large%@",
+ [input unsignedLongLongValue], [context fieldDescription]);
+
+ } else {
+ return [FSTIntegerValue integerValue:(int64_t)extended];
+ }
+ }
+
+ case 'f':
+ return [FSTDoubleValue doubleValue:[input doubleValue]];
+
+ case 'd':
+ // Double values are already the right type, so just reuse the existing boxed double.
+ //
+ // Note that NSNumber already performs NaN normalization to a single shared instance
+ // so there's no need to treat NaN specially here.
+ return [FSTDoubleValue doubleValue:[input doubleValue]];
+
+ case 'B': // Falls through.
+ case 'c': // Falls through.
+ case 'C':
+ // Boolean values are weird.
+ //
+ // On arm64, objCType of a BOOL-valued NSNumber will be "c", even though @encode(BOOL)
+ // returns "B". "c" is the same as @encode(signed char). Unfortunately this means that
+ // legitimate usage of signed chars is impossible, but this should be rare.
+ //
+ // Additionally, for consistency, map unsigned chars to bools in the same way.
+ return [FSTBooleanValue booleanValue:[input boolValue]];
+
+ default:
+ // All documented codes should be handled above, so this shouldn't happen.
+ FSTCFail(@"Unknown NSNumber objCType %s on %@", cType, input);
+ }
+
+ } else if ([input isKindOfClass:[NSString class]]) {
+ return [FSTStringValue stringValue:input];
+
+ } else if ([input isKindOfClass:[NSDate class]]) {
+ return [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:input]];
+
+ } else if ([input isKindOfClass:[FIRGeoPoint class]]) {
+ return [FSTGeoPointValue geoPointValue:input];
+
+ } else if ([input isKindOfClass:[NSData class]]) {
+ return [FSTBlobValue blobValue:input];
+
+ } else if ([input isKindOfClass:[FSTDocumentKeyReference class]]) {
+ FSTDocumentKeyReference *reference = input;
+ if (![reference.databaseID isEqual:self.databaseID]) {
+ FSTDatabaseID *other = reference.databaseID;
+ FSTThrowInvalidArgument(
+ @"Document Reference is for database %@/%@ but should be for database %@/%@%@",
+ other.projectID, other.databaseID, self.databaseID.projectID, self.databaseID.databaseID,
+ [context fieldDescription]);
+ }
+ return [FSTReferenceValue referenceValue:reference.key databaseID:self.databaseID];
+
+ } else if ([input isKindOfClass:[FIRFieldValue class]]) {
+ if ([input isKindOfClass:[FSTDeleteFieldValue class]]) {
+ // We shouldn't encounter delete sentinels here. Provide a good error.
+ if (context.dataSource != FSTUserDataSourceUpdate) {
+ FSTThrowInvalidArgument(@"FieldValue.delete() can only be used with updateData().");
+ } else {
+ FSTAssert(context.path.length > 0,
+ @"FieldValue.delete() at the top level should have already been handled.");
+ FSTThrowInvalidArgument(
+ @"FieldValue.delete() can only appear at the top level of your "
+ "update data%@",
+ [context fieldDescription]);
+ }
+ } else if ([input isKindOfClass:[FSTServerTimestampFieldValue class]]) {
+ if (context.dataSource != FSTUserDataSourceSet &&
+ context.dataSource != FSTUserDataSourceUpdate) {
+ FSTThrowInvalidArgument(
+ @"FieldValue.serverTimestamp() can only be used with setData() and updateData().");
+ }
+ if (!context.path) {
+ FSTThrowInvalidArgument(
+ @"FieldValue.serverTimestamp() is not currently supported inside arrays%@",
+ [context fieldDescription]);
+ }
+ [context.fieldTransforms
+ addObject:[[FSTFieldTransform alloc]
+ initWithPath:context.path
+ transform:[FSTServerTimestampTransform serverTimestampTransform]]];
+
+ // Return nil so this value is omitted from the parsed result.
+ return nil;
+ } else {
+ FSTFail(@"Unknown FIRFieldValue type: %@", NSStringFromClass([input class]));
+ }
+
+ } else {
+ FSTThrowInvalidArgument(@"Unsupported type: %@%@", NSStringFromClass([input class]),
+ [context fieldDescription]);
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Auth/FSTCredentialsProvider.h b/Firestore/Source/Auth/FSTCredentialsProvider.h
new file mode 100644
index 0000000..eb591ab
--- /dev/null
+++ b/Firestore/Source/Auth/FSTCredentialsProvider.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRApp;
+@class FSTDispatchQueue;
+@class FSTUser;
+
+#pragma mark - FSTGetTokenResult
+
+/**
+ * The current FSTUser and the authentication token provided by the underlying authentication
+ * mechanism. This is the result of calling -[FSTCredentialsProvider getTokenForcingRefresh].
+ *
+ * ## Portability notes: no TokenType on iOS
+ *
+ * The TypeScript client supports 1st party Oauth tokens (for the Firebase Console to auth as the
+ * developer) and OAuth2 tokens for the node.js sdk to auth with a service account. We don't have
+ * plans to support either case on mobile so there's no TokenType here.
+ */
+// TODO(mcg): Rename FSTToken, change parameter order to line up with the other platforms.
+@interface FSTGetTokenResult : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithUser:(FSTUser *)user
+ token:(NSString *_Nullable)token NS_DESIGNATED_INITIALIZER;
+
+/** The user with which the token is associated (used for persisting user state on disk, etc.). */
+@property(nonatomic, nonnull, readonly) FSTUser *user;
+
+/** The actual raw token. */
+@property(nonatomic, copy, nullable, readonly) NSString *token;
+
+@end
+
+#pragma mark - Typedefs
+
+/**
+ * `FSTVoidTokenErrorBlock` is a block that gets a token or an error.
+ *
+ * @param token An auth token as a string.
+ * @param error The error if one occurred, or else `nil`.
+ */
+typedef void (^FSTVoidGetTokenResultBlock)(FSTGetTokenResult *_Nullable token,
+ NSError *_Nullable error);
+
+/** Listener block notified with an FSTUser. */
+typedef void (^FSTVoidUserBlock)(FSTUser *user);
+
+#pragma mark - FSTCredentialsProvider
+
+/** Provides methods for getting the uid and token for the current user and listen for changes. */
+@protocol FSTCredentialsProvider<NSObject>
+
+/** Requests token for the current user, optionally forcing a refreshed token to be fetched. */
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh completion:(FSTVoidGetTokenResultBlock)completion;
+
+/**
+ * A listener to be notified of user changes (sign-in / sign-out). It is immediately called once
+ * with the initial user.
+ *
+ * Note that this block will be called back on an arbitrary thread that is not the normal Firestore
+ * worker thread.
+ */
+@property(nonatomic, copy, nullable, readwrite) FSTVoidUserBlock userChangeListener;
+
+@end
+
+#pragma mark - FSTFirebaseCredentialsProvider
+
+/**
+ * `FSTFirebaseCredentialsProvider` uses Firebase Auth via `FIRApp` to get an auth token.
+ *
+ * NOTE: To simplify the implementation, it requires that you set `userChangeListener` with a
+ * non-`nil` value no more than once and don't call `getTokenForcingRefresh:` after setting
+ * it to `nil`.
+ *
+ * This class must be implemented in a thread-safe manner since it is accessed from the thread
+ * backing our internal worker queue and the callbacks from FIRAuth will be executed on an
+ * arbitrary different thread.
+ */
+@interface FSTFirebaseCredentialsProvider : NSObject<FSTCredentialsProvider>
+
+/**
+ * Initializes a new FSTFirebaseCredentialsProvider.
+ *
+ * @param app The Firebase app from which to get credentials.
+ *
+ * @return A new instance of FSTFirebaseCredentialsProvider.
+ */
+- (instancetype)initWithApp:(FIRApp *)app NS_DESIGNATED_INITIALIZER;
+
+- (id)init NS_UNAVAILABLE;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Auth/FSTCredentialsProvider.m b/Firestore/Source/Auth/FSTCredentialsProvider.m
new file mode 100644
index 0000000..cec7c2b
--- /dev/null
+++ b/Firestore/Source/Auth/FSTCredentialsProvider.m
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTCredentialsProvider.h"
+
+#import <FirebaseCommunity/FIRApp.h>
+#import <FirebaseCommunity/FIRAuth.h>
+#import <FirebaseCommunity/FIRUser.h>
+#import <GRPCClient/GRPCCall.h>
+
+// This is not an exported header so it's not visible via FirebaseCommunity
+#import "FIRAppInternal.h"
+
+#import "FIRFirestoreErrors.h"
+#import "FSTAssert.h"
+#import "FSTClasses.h"
+#import "FSTDispatchQueue.h"
+#import "FSTUser.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTGetTokenResult
+
+@implementation FSTGetTokenResult
+- (instancetype)initWithUser:(FSTUser *)user token:(NSString *_Nullable)token {
+ if (self = [super init]) {
+ _user = user;
+ _token = token;
+ }
+ return self;
+}
+@end
+
+#pragma mark - FSTFirebaseCredentialsProvider
+// TODO(mikelehen): Currently, we have a strong dependency on FIRAuth but we should ideally use
+// only internal APIs on FIRApp instead. However, currently the FIRApp internal APIs don't expose
+// the uid of the current user and don't expose an auth state change listener. So we use FIRAuth.
+@interface FSTFirebaseCredentialsProvider ()
+
+@property(nonatomic, strong, readonly) FIRApp *app;
+@property(nonatomic, strong, readonly) FIRAuth *auth;
+
+/** Handle used to stop receiving auth changes once userChangeListener is removed. */
+@property(nonatomic, strong, nullable, readwrite)
+ FIRAuthStateDidChangeListenerHandle authListenerHandle;
+
+/** The current user as reported to us via our AuthStateDidChangeListener. */
+@property(nonatomic, strong, nonnull, readwrite) FSTUser *currentUser;
+
+/**
+ * Counter used to detect if the user changed while a -getTokenForcingRefresh: request was
+ * outstanding.
+ */
+@property(nonatomic, assign, readwrite) int userCounter;
+
+@end
+
+@implementation FSTFirebaseCredentialsProvider {
+ FSTVoidUserBlock _userChangeListener;
+}
+
+- (instancetype)initWithApp:(FIRApp *)app {
+ self = [super init];
+ if (self) {
+ _app = app;
+ _auth = [FIRAuth authWithApp:app];
+ _currentUser = [[FSTUser alloc] initWithUID:self.auth.currentUser.uid];
+ _userCounter = 0;
+
+ // Register for user changes so that we can internally track the current user.
+ FSTWeakify(self);
+ _authListenerHandle = [self.auth addAuthStateDidChangeListener:^(FIRAuth *auth, FIRUser *user) {
+ FSTStrongify(self);
+ if (self) {
+ @synchronized(self) {
+ FSTUser *newUser = [[FSTUser alloc] initWithUID:user.uid];
+ if (![newUser isEqual:self.currentUser]) {
+ self.currentUser = newUser;
+ self.userCounter++;
+ FSTVoidUserBlock listenerBlock = self.userChangeListener;
+ if (listenerBlock) {
+ listenerBlock(self.currentUser);
+ }
+ }
+ }
+ }
+ }];
+ }
+ return self;
+}
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(FSTVoidGetTokenResultBlock)completion {
+ FSTAssert(self.authListenerHandle, @"getToken cannot be called after listener removed.");
+
+ // Take note of the current value of the userCounter so that this method can fail (with a
+ // FIRFirestoreErrorCodeAborted error) if there is a user change while the request is outstanding.
+ int initialUserCounter = self.userCounter;
+
+ void (^getTokenCallback)(NSString *, NSError *) = ^(NSString *_Nullable token,
+ NSError *_Nullable error) {
+ @synchronized(self) {
+ if (initialUserCounter != self.userCounter) {
+ // Cancel the request since the user changed while the request was outstanding so the
+ // response is likely for a previous user (which user, we can't be sure).
+ NSDictionary *errorInfo = @{ @"details" : @"getToken aborted due to user change." };
+ NSError *cancelError = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeAborted
+ userInfo:errorInfo];
+ completion(nil, cancelError);
+ } else {
+ FSTGetTokenResult *result =
+ [[FSTGetTokenResult alloc] initWithUser:self.currentUser token:token];
+ completion(result, error);
+ }
+ };
+ };
+
+ [self.app getTokenForcingRefresh:forceRefresh withCallback:getTokenCallback];
+}
+
+- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block {
+ @synchronized(self) {
+ if (block) {
+ FSTAssert(!_userChangeListener, @"UserChangeListener set twice!");
+
+ // Fire initial event.
+ block(self.currentUser);
+ } else {
+ FSTAssert(self.authListenerHandle, @"UserChangeListener removed twice!");
+ FSTAssert(_userChangeListener, @"UserChangeListener removed without being set!");
+ [self.auth removeAuthStateDidChangeListener:self.authListenerHandle];
+
+ self.authListenerHandle = nil;
+ }
+ _userChangeListener = block;
+ }
+}
+
+- (nullable FSTVoidUserBlock)userChangeListener {
+ @synchronized(self) {
+ return _userChangeListener;
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Auth/FSTEmptyCredentialsProvider.h b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.h
new file mode 100644
index 0000000..c0074c2
--- /dev/null
+++ b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.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 "FSTCredentialsProvider.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** `FSTEmptyCredentialsProvider` always yields an empty token. */
+@interface FSTEmptyCredentialsProvider : NSObject<FSTCredentialsProvider>
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m
new file mode 100644
index 0000000..266e09b
--- /dev/null
+++ b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m
@@ -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 "FSTEmptyCredentialsProvider.h"
+
+#import "FSTUser.h"
+#import "FSTAssert.h"
+#import "FSTDispatchQueue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTEmptyCredentialsProvider
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh
+ completion:(FSTVoidGetTokenResultBlock)completion {
+ completion(nil, nil);
+}
+
+- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block {
+ // Since the user never changes, we just need to fire the initial event and don't need to hang
+ // onto the block.
+ if (block) {
+ block([FSTUser unauthenticatedUser]);
+ }
+}
+
+- (nullable FSTVoidUserBlock)userChangeListener {
+ // TODO(mikelehen): Implementation omitted for convenience since it's not actually required.
+ FSTFail(@"Not implemented.");
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Auth/FSTUser.h b/Firestore/Source/Auth/FSTUser.h
new file mode 100644
index 0000000..83b1962
--- /dev/null
+++ b/Firestore/Source/Auth/FSTUser.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Simple wrapper around a nullable UID. Mostly exists to make code more readable and for use as
+ * a key in dictionaries (since keys cannot be nil).
+ */
+@interface FSTUser : NSObject<NSCopying>
+
+/** Returns an FSTUser with a nil UID. */
++ (instancetype)unauthenticatedUser;
+
+// Porting note: no GOOGLE_CREDENTIALS or FIRST_PARTY equivalent on iOS, see FSTGetTokenResult for
+// more details.
+
+/** Initializes an FSTUser with the given UID. */
+- (instancetype)initWithUID:(NSString *_Nullable)UID NS_DESIGNATED_INITIALIZER;
+- (instancetype)init NS_UNAVAILABLE;
+
+@property(nonatomic, copy, nullable, readonly) NSString *UID;
+
+@property(nonatomic, assign, readonly, getter=isUnauthenticated) BOOL unauthenticated;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Auth/FSTUser.m b/Firestore/Source/Auth/FSTUser.m
new file mode 100644
index 0000000..a7492b2
--- /dev/null
+++ b/Firestore/Source/Auth/FSTUser.m
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTUser.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTUser
+
+@implementation FSTUser
+
+@dynamic unauthenticated;
+
++ (instancetype)unauthenticatedUser {
+ return [[FSTUser alloc] initWithUID:nil];
+}
+
+- (instancetype)initWithUID:(NSString *_Nullable)UID {
+ if (self = [super init]) {
+ _UID = UID;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ } else if (![object isKindOfClass:[FSTUser class]]) {
+ return NO;
+ } else {
+ FSTUser *other = object;
+ return (self.isUnauthenticated && other.isUnauthenticated) ||
+ [self.UID isEqualToString:other.UID];
+ }
+}
+
+- (NSUInteger)hash {
+ return [self.UID hash];
+}
+
+- (id)copyWithZone:(nullable NSZone *)zone {
+ return self; // since FSTUser objects are immutable
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTUser uid=%@>", self.UID];
+}
+
+- (BOOL)isUnauthenticated {
+ return self.UID == nil;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTDatabaseInfo.h b/Firestore/Source/Core/FSTDatabaseInfo.h
new file mode 100644
index 0000000..fae884f
--- /dev/null
+++ b/Firestore/Source/Core/FSTDatabaseInfo.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>
+
+@class FSTDatabaseID;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** FSTDatabaseInfo contains data about the database. */
+@interface FSTDatabaseInfo : NSObject
+
+/**
+ * Creates and returns a new FSTDatabaseInfo.
+ *
+ * @param databaseID The project/database to use.
+ * @param persistenceKey A unique identifier for this Firestore's local storage. Usually derived
+ * from -[FIRApp appName].
+ * @param host The hostname of the datastore backend.
+ * @param sslEnabled Whether to use SSL when connecting.
+ * @return A new instance of FSTDatabaseInfo.
+ */
++ (instancetype)databaseInfoWithDatabaseID:(FSTDatabaseID *)databaseID
+ persistenceKey:(NSString *)persistenceKey
+ host:(NSString *)host
+ sslEnabled:(BOOL)sslEnabled;
+
+/** The database info. */
+@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID;
+
+/** The application name, taken from FIRApp. */
+@property(nonatomic, copy, readonly) NSString *persistenceKey;
+
+/** The hostname of the backend. */
+@property(nonatomic, copy, readonly) NSString *host;
+
+/** Whether to use SSL when connecting. */
+@property(nonatomic, readonly, getter=isSSLEnabled) BOOL sslEnabled;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTDatabaseInfo.m b/Firestore/Source/Core/FSTDatabaseInfo.m
new file mode 100644
index 0000000..d2cd0ed
--- /dev/null
+++ b/Firestore/Source/Core/FSTDatabaseInfo.m
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTDatabaseInfo.h"
+
+#import "FSTDatabaseID.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTDatabaseInfo
+
+@implementation FSTDatabaseInfo
+
+#pragma mark - Constructors
+
++ (instancetype)databaseInfoWithDatabaseID:(FSTDatabaseID *)databaseID
+ persistenceKey:(NSString *)persistenceKey
+ host:(NSString *)host
+ sslEnabled:(BOOL)sslEnabled {
+ return [[FSTDatabaseInfo alloc] initWithDatabaseID:databaseID
+ persistenceKey:persistenceKey
+ host:host
+ sslEnabled:sslEnabled];
+}
+
+/**
+ * Designated initializer.
+ *
+ * @param databaseID The database in the datastore.
+ * @param persistenceKey A unique identifier for this Firestore's local storage. Usually derived
+ * from -[FIRApp appName].
+ * @param host The Firestore server hostname.
+ * @param sslEnabled Whether to use SSL when connecting.
+ */
+- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID
+ persistenceKey:(NSString *)persistenceKey
+ host:(NSString *)host
+ sslEnabled:(BOOL)sslEnabled {
+ if (self = [super init]) {
+ _databaseID = databaseID;
+ _persistenceKey = [persistenceKey copy];
+ _host = [host copy];
+ _sslEnabled = sslEnabled;
+ }
+ return self;
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString
+ stringWithFormat:@"<FSTDatabaseInfo: databaseID:%@ host:%@>", self.databaseID, self.host];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTEventManager.h b/Firestore/Source/Core/FSTEventManager.h
new file mode 100644
index 0000000..43ada66
--- /dev/null
+++ b/Firestore/Source/Core/FSTEventManager.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTRemoteStore.h"
+#import "FSTTypes.h"
+#import "FSTViewSnapshot.h"
+
+@class FSTQuery;
+@class FSTSyncEngine;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTListenOptions
+
+@interface FSTListenOptions : NSObject
+
++ (instancetype)defaultOptions;
+
+- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges
+ includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges
+ waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline
+ NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+@property(nonatomic, assign, readonly) BOOL includeQueryMetadataChanges;
+
+@property(nonatomic, assign, readonly) BOOL includeDocumentMetadataChanges;
+
+@property(nonatomic, assign, readonly) BOOL waitForSyncWhenOnline;
+
+@end
+
+#pragma mark - FSTQueryListener
+
+/**
+ * FSTQueryListener takes a series of internal view snapshots and determines when to raise
+ * user-facing events.
+ */
+@interface FSTQueryListener : NSObject
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ options:(FSTListenOptions *)options
+ viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot;
+- (void)queryDidError:(NSError *)error;
+- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState;
+
+@property(nonatomic, strong, readonly) FSTQuery *query;
+
+@end
+
+#pragma mark - FSTEventManager
+
+/**
+ * EventManager is responsible for mapping queries to query event emitters. It handles "fan-out."
+ * (Identical queries will re-use the same watch on the backend.)
+ */
+@interface FSTEventManager : NSObject <FSTOnlineStateDelegate>
+
++ (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine;
+
+- (instancetype)init __attribute__((unavailable("Use static constructor method.")));
+
+- (FSTTargetID)addListener:(FSTQueryListener *)listener;
+- (void)removeListener:(FSTQueryListener *)listener;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTEventManager.m b/Firestore/Source/Core/FSTEventManager.m
new file mode 100644
index 0000000..17a0546
--- /dev/null
+++ b/Firestore/Source/Core/FSTEventManager.m
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTEventManager.h"
+
+#import "FSTAssert.h"
+#import "FSTDocumentSet.h"
+#import "FSTQuery.h"
+#import "FSTSyncEngine.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTListenOptions
+
+@implementation FSTListenOptions
+
++ (instancetype)defaultOptions {
+ static FSTListenOptions *defaultOptions;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ defaultOptions = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO
+ includeDocumentMetadataChanges:NO
+ waitForSyncWhenOnline:NO];
+ });
+ return defaultOptions;
+}
+
+- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges
+ includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges
+ waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline {
+ if (self = [super init]) {
+ _includeQueryMetadataChanges = includeQueryMetadataChanges;
+ _includeDocumentMetadataChanges = includeDocumentMetadataChanges;
+ _waitForSyncWhenOnline = waitForSyncWhenOnline;
+ }
+ return self;
+}
+
+- (instancetype)init {
+ FSTFail(@"FSTListenOptions init not supported");
+ return nil;
+}
+
+@end
+
+#pragma mark - FSTQueryListenersInfo
+
+/**
+ * Holds the listeners and the last received ViewSnapshot for a query being tracked by
+ * EventManager.
+ */
+@interface FSTQueryListenersInfo : NSObject
+@property(nonatomic, strong, nullable, readwrite) FSTViewSnapshot *viewSnapshot;
+@property(nonatomic, assign, readwrite) FSTTargetID targetID;
+@property(nonatomic, strong, readonly) NSMutableArray<FSTQueryListener *> *listeners;
+@end
+
+@implementation FSTQueryListenersInfo
+- (instancetype)init {
+ if (self = [super init]) {
+ _listeners = [NSMutableArray array];
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - FSTQueryListener
+
+@interface FSTQueryListener ()
+
+/** The last received view snapshot. */
+@property(nonatomic, strong, nullable) FSTViewSnapshot *snapshot;
+
+@property(nonatomic, strong, readonly) FSTListenOptions *options;
+
+/**
+ * Initial snapshots (e.g. from cache) may not be propagated to the FSTViewSnapshotHandler.
+ * This flag is set to YES once we've actually raised an event.
+ */
+@property(nonatomic, assign, readwrite) BOOL raisedInitialEvent;
+
+/** The last online state this query listener got. */
+@property(nonatomic, assign, readwrite) FSTOnlineState onlineState;
+
+/** The FSTViewSnapshotHandler associated with this query listener. */
+@property(nonatomic, copy, nullable) FSTViewSnapshotHandler viewSnapshotHandler;
+
+@end
+
+@implementation FSTQueryListener
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ options:(FSTListenOptions *)options
+ viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler {
+ if (self = [super init]) {
+ _query = query;
+ _options = options;
+ _viewSnapshotHandler = viewSnapshotHandler;
+ _raisedInitialEvent = NO;
+ }
+ return self;
+}
+
+- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot {
+ FSTAssert(snapshot.documentChanges.count > 0 || snapshot.syncStateChanged,
+ @"We got a new snapshot with no changes?");
+
+ if (!self.options.includeDocumentMetadataChanges) {
+ // Remove the metadata-only changes.
+ NSMutableArray<FSTDocumentViewChange *> *changes = [NSMutableArray array];
+ for (FSTDocumentViewChange *change in snapshot.documentChanges) {
+ if (change.type != FSTDocumentViewChangeTypeMetadata) {
+ [changes addObject:change];
+ }
+ }
+ snapshot = [[FSTViewSnapshot alloc] initWithQuery:snapshot.query
+ documents:snapshot.documents
+ oldDocuments:snapshot.oldDocuments
+ documentChanges:changes
+ fromCache:snapshot.fromCache
+ hasPendingWrites:snapshot.hasPendingWrites
+ syncStateChanged:snapshot.syncStateChanged];
+ }
+
+ if (!self.raisedInitialEvent) {
+ if ([self shouldRaiseInitialEventForSnapshot:snapshot onlineState:self.onlineState]) {
+ [self raiseInitialEventForSnapshot:snapshot];
+ }
+ } else if ([self shouldRaiseEventForSnapshot:snapshot]) {
+ self.viewSnapshotHandler(snapshot, nil);
+ }
+
+ self.snapshot = snapshot;
+}
+
+- (void)queryDidError:(NSError *)error {
+ self.viewSnapshotHandler(nil, error);
+}
+
+- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState {
+ self.onlineState = onlineState;
+ if (self.snapshot && !self.raisedInitialEvent &&
+ [self shouldRaiseInitialEventForSnapshot:self.snapshot onlineState:onlineState]) {
+ [self raiseInitialEventForSnapshot:self.snapshot];
+ }
+}
+
+- (BOOL)shouldRaiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot
+ onlineState:(FSTOnlineState)onlineState {
+ FSTAssert(!self.raisedInitialEvent,
+ @"Determining whether to raise initial event, but already had first event.");
+
+ // Always raise the first event when we're synced
+ if (!snapshot.fromCache) {
+ return YES;
+ }
+
+ // NOTE: We consider OnlineState.Unknown as online (it should become Failed
+ // or Online if we wait long enough).
+ BOOL maybeOnline = onlineState != FSTOnlineStateFailed;
+ // Don't raise the event if we're online, aren't synced yet (checked
+ // above) and are waiting for a sync.
+ if (self.options.waitForSyncWhenOnline && maybeOnline) {
+ FSTAssert(snapshot.fromCache, @"Waiting for sync, but snapshot is not from cache.");
+ return NO;
+ }
+
+ // Raise data from cache if we have any documents or we are offline
+ return !snapshot.documents.isEmpty || onlineState == FSTOnlineStateFailed;
+}
+
+- (BOOL)shouldRaiseEventForSnapshot:(FSTViewSnapshot *)snapshot {
+ // We don't need to handle includeDocumentMetadataChanges here because the Metadata only changes
+ // have already been stripped out if needed. At this point the only changes we will see are the
+ // ones we should propagate.
+ if (snapshot.documentChanges.count > 0) {
+ return YES;
+ }
+
+ BOOL hasPendingWritesChanged =
+ self.snapshot && self.snapshot.hasPendingWrites != snapshot.hasPendingWrites;
+ if (snapshot.syncStateChanged || hasPendingWritesChanged) {
+ return self.options.includeQueryMetadataChanges;
+ }
+
+ // Generally we should have hit one of the cases above, but it's possible to get here if there
+ // were only metadata docChanges and they got stripped out.
+ return NO;
+}
+
+- (void)raiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot {
+ FSTAssert(!self.raisedInitialEvent, @"Trying to raise initial events for second time");
+ snapshot = [[FSTViewSnapshot alloc]
+ initWithQuery:snapshot.query
+ documents:snapshot.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snapshot.query.comparator]
+ documentChanges:[FSTQueryListener getInitialViewChangesFor:snapshot]
+ fromCache:snapshot.fromCache
+ hasPendingWrites:snapshot.hasPendingWrites
+ syncStateChanged:YES];
+ self.raisedInitialEvent = YES;
+ self.viewSnapshotHandler(snapshot, nil);
+}
+
++ (NSArray<FSTDocumentViewChange *> *)getInitialViewChangesFor:(FSTViewSnapshot *)snapshot {
+ NSMutableArray<FSTDocumentViewChange *> *result = [NSMutableArray array];
+ for (FSTDocument *doc in snapshot.documents.documentEnumerator) {
+ [result addObject:[FSTDocumentViewChange changeWithDocument:doc
+ type:FSTDocumentViewChangeTypeAdded]];
+ }
+ return result;
+}
+
+@end
+
+#pragma mark - FSTEventManager
+
+@interface FSTEventManager () <FSTSyncEngineDelegate>
+
+- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine;
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTQuery *, FSTQueryListenersInfo *> *queries;
+@property(nonatomic, assign) FSTOnlineState onlineState;
+
+@end
+
+@implementation FSTEventManager
+
++ (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine {
+ return [[FSTEventManager alloc] initWithSyncEngine:syncEngine];
+}
+
+- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine {
+ if (self = [super init]) {
+ _syncEngine = syncEngine;
+ _queries = [NSMutableDictionary dictionary];
+
+ _syncEngine.delegate = self;
+ }
+ return self;
+}
+
+- (FSTTargetID)addListener:(FSTQueryListener *)listener {
+ FSTQuery *query = listener.query;
+ BOOL firstListen = NO;
+
+ FSTQueryListenersInfo *queryInfo = self.queries[query];
+ if (!queryInfo) {
+ firstListen = YES;
+ queryInfo = [[FSTQueryListenersInfo alloc] init];
+ self.queries[query] = queryInfo;
+ }
+ [queryInfo.listeners addObject:listener];
+
+ [listener clientDidChangeOnlineState:self.onlineState];
+
+ if (queryInfo.viewSnapshot) {
+ [listener queryDidChangeViewSnapshot:queryInfo.viewSnapshot];
+ }
+
+ if (firstListen) {
+ queryInfo.targetID = [self.syncEngine listenToQuery:query];
+ }
+ return queryInfo.targetID;
+}
+
+- (void)removeListener:(FSTQueryListener *)listener {
+ FSTQuery *query = listener.query;
+ BOOL lastListen = NO;
+
+ FSTQueryListenersInfo *queryInfo = self.queries[query];
+ if (queryInfo) {
+ [queryInfo.listeners removeObject:listener];
+ lastListen = (queryInfo.listeners.count == 0);
+ }
+
+ if (lastListen) {
+ [self.queries removeObjectForKey:query];
+ [self.syncEngine stopListeningToQuery:query];
+ }
+}
+
+- (void)handleViewSnapshots:(NSArray<FSTViewSnapshot *> *)viewSnapshots {
+ for (FSTViewSnapshot *viewSnapshot in viewSnapshots) {
+ FSTQuery *query = viewSnapshot.query;
+ FSTQueryListenersInfo *queryInfo = self.queries[query];
+ if (queryInfo) {
+ for (FSTQueryListener *listener in queryInfo.listeners) {
+ [listener queryDidChangeViewSnapshot:viewSnapshot];
+ }
+ queryInfo.viewSnapshot = viewSnapshot;
+ }
+ }
+}
+
+- (void)handleError:(NSError *)error forQuery:(FSTQuery *)query {
+ FSTQueryListenersInfo *queryInfo = self.queries[query];
+ if (queryInfo) {
+ for (FSTQueryListener *listener in queryInfo.listeners) {
+ [listener queryDidError:error];
+ }
+ }
+
+ // Remove all listeners. NOTE: We don't need to call [FSTSyncEngine stopListening] after an error.
+ [self.queries removeObjectForKey:query];
+}
+
+- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState {
+ self.onlineState = onlineState;
+ for (FSTQueryListenersInfo *info in self.queries.objectEnumerator) {
+ for (FSTQueryListener *listener in info.listeners) {
+ [listener clientDidChangeOnlineState:onlineState];
+ }
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h
new file mode 100644
index 0000000..45f13cc
--- /dev/null
+++ b/Firestore/Source/Core/FSTFirestoreClient.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FSTRemoteStore.h"
+#import "FSTTypes.h"
+#import "FSTViewSnapshot.h"
+
+@class FSTDatabaseID;
+@class FSTDatabaseInfo;
+@class FSTDispatchQueue;
+@class FSTDocument;
+@class FSTListenOptions;
+@class FSTMutation;
+@class FSTQuery;
+@class FSTQueryListener;
+@class FSTTransaction;
+@protocol FSTCredentialsProvider;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FirestoreClient is a top-level class that constructs and owns all of the pieces of the client
+ * SDK architecture. It is responsible for creating the worker queue that is shared by all of the
+ * other components in the system.
+ */
+@interface FSTFirestoreClient : NSObject
+
+/**
+ * Creates and returns a FSTFirestoreClient with the given parameters.
+ *
+ * All callbacks and events will be triggered on the provided userDispatchQueue.
+ */
++ (instancetype)clientWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo
+ usePersistence:(BOOL)usePersistence
+ credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider
+ userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue;
+
+- (instancetype)init __attribute__((unavailable("Use static constructor method.")));
+
+/** Shuts down this client, cancels all writes / listeners, and releases all resources. */
+- (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion;
+
+/** Starts listening to a query. */
+- (FSTQueryListener *)listenToQuery:(FSTQuery *)query
+ options:(FSTListenOptions *)options
+ viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler;
+
+/** Stops listening to a query previously listened to. */
+- (void)removeListener:(FSTQueryListener *)listener;
+
+/** Write mutations. completion will be notified when it's written to the backend. */
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations
+ completion:(nullable FSTVoidErrorBlock)completion;
+
+/** Tries to execute the transaction in updateBlock up to retries times. */
+- (void)transactionWithRetries:(int)retries
+ updateBlock:(FSTTransactionBlock)updateBlock
+ completion:(FSTVoidIDErrorBlock)completion;
+
+/** The database ID of the databaseInfo this client was initialized with. */
+@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID;
+
+/**
+ * Dispatch queue for user callbacks / events. This will often be the "Main Dispatch Queue" of the
+ * app but the developer can configure it to a different queue if they so choose.
+ */
+@property(nonatomic, strong, readonly) FSTDispatchQueue *userDispatchQueue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTFirestoreClient.m b/Firestore/Source/Core/FSTFirestoreClient.m
new file mode 100644
index 0000000..2066ce9
--- /dev/null
+++ b/Firestore/Source/Core/FSTFirestoreClient.m
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTFirestoreClient.h"
+
+#import "FSTAssert.h"
+#import "FSTClasses.h"
+#import "FSTCredentialsProvider.h"
+#import "FSTDatabaseInfo.h"
+#import "FSTDatastore.h"
+#import "FSTDispatchQueue.h"
+#import "FSTEagerGarbageCollector.h"
+#import "FSTEventManager.h"
+#import "FSTLevelDB.h"
+#import "FSTLocalSerializer.h"
+#import "FSTLocalStore.h"
+#import "FSTLogger.h"
+#import "FSTMemoryPersistence.h"
+#import "FSTNoOpGarbageCollector.h"
+#import "FSTRemoteStore.h"
+#import "FSTSerializerBeta.h"
+#import "FSTSyncEngine.h"
+#import "FSTTransaction.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTFirestoreClient ()
+- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo
+ usePersistence:(BOOL)usePersistence
+ credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider
+ userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue
+ workerDispatchQueue:(FSTDispatchQueue *)queue NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo;
+@property(nonatomic, strong, readonly) FSTEventManager *eventManager;
+@property(nonatomic, strong, readonly) id<FSTPersistence> persistence;
+@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine;
+@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore;
+@property(nonatomic, strong, readonly) FSTLocalStore *localStore;
+
+/**
+ * Dispatch queue responsible for all of our internal processing. When we get incoming work from
+ * the user (via public API) or the network (incoming GRPC messages), we should always dispatch
+ * onto this queue. This ensures our internal data structures are never accessed from multiple
+ * threads simultaneously.
+ */
+@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue;
+
+@property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentialsProvider;
+
+@end
+
+@implementation FSTFirestoreClient
+
++ (instancetype)clientWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo
+ usePersistence:(BOOL)usePersistence
+ credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider
+ userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue {
+ return [[FSTFirestoreClient alloc] initWithDatabaseInfo:databaseInfo
+ usePersistence:usePersistence
+ credentialsProvider:credentialsProvider
+ userDispatchQueue:userDispatchQueue
+ workerDispatchQueue:workerDispatchQueue];
+}
+
+- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo
+ usePersistence:(BOOL)usePersistence
+ credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider
+ userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue {
+ if (self = [super init]) {
+ _databaseInfo = databaseInfo;
+ _credentialsProvider = credentialsProvider;
+ _userDispatchQueue = userDispatchQueue;
+ _workerDispatchQueue = workerDispatchQueue;
+
+ dispatch_semaphore_t initialUserAvailable = dispatch_semaphore_create(0);
+ __block FSTUser *initialUser;
+ FSTWeakify(self);
+ _credentialsProvider.userChangeListener = ^(FSTUser *user) {
+ FSTStrongify(self);
+ if (self) {
+ if (!initialUser) {
+ initialUser = user;
+ dispatch_semaphore_signal(initialUserAvailable);
+ } else {
+ [workerDispatchQueue dispatchAsync:^{
+ [self userDidChange:user];
+ }];
+ }
+ }
+ };
+
+ // Defer initialization until we get the current user from the userChangeListener. This is
+ // guaranteed to be synchronously dispatched onto our worker queue, so we will be initialized
+ // before any subsequently queued work runs.
+ [_workerDispatchQueue dispatchAsync:^{
+ dispatch_semaphore_wait(initialUserAvailable, DISPATCH_TIME_FOREVER);
+
+ [self initializeWithUser:initialUser usePersistence:usePersistence];
+ }];
+ }
+ return self;
+}
+
+- (void)initializeWithUser:(FSTUser *)user usePersistence:(BOOL)usePersistence {
+ // Do all of our initialization on our own dispatch queue.
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ // Note: The initialization work must all be synchronous (we can't dispatch more work) since
+ // external write/listen operations could get queued to run before that subsequent work
+ // completes.
+ id<FSTGarbageCollector> garbageCollector;
+ if (usePersistence) {
+ // TODO(http://b/33384523): For now we just disable garbage collection when persistence is
+ // enabled.
+ garbageCollector = [[FSTNoOpGarbageCollector alloc] init];
+
+ NSString *dir = [FSTLevelDB storageDirectoryForDatabaseInfo:self.databaseInfo
+ documentsDirectory:[FSTLevelDB documentsDirectory]];
+
+ FSTSerializerBeta *remoteSerializer =
+ [[FSTSerializerBeta alloc] initWithDatabaseID:self.databaseInfo.databaseID];
+ FSTLocalSerializer *serializer =
+ [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer];
+
+ _persistence = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer];
+ } else {
+ garbageCollector = [[FSTEagerGarbageCollector alloc] init];
+ _persistence = [FSTMemoryPersistence persistence];
+ }
+
+ NSError *error;
+ if (![_persistence start:&error]) {
+ // If local storage fails to start then just throw up our hands: the error is unrecoverable.
+ // There's nothing an end-user can do and nearly all failures indicate the developer is doing
+ // something grossly wrong so we should stop them cold in their tracks with a failure they
+ // can't ignore.
+ [NSException raise:NSInternalInconsistencyException format:@"Failed to open DB: %@", error];
+ }
+
+ _localStore = [[FSTLocalStore alloc] initWithPersistence:_persistence
+ garbageCollector:garbageCollector
+ initialUser:user];
+
+ FSTDatastore *datastore = [FSTDatastore datastoreWithDatabase:self.databaseInfo
+ workerDispatchQueue:self.workerDispatchQueue
+ credentials:self.credentialsProvider];
+
+ _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:datastore];
+
+ _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore
+ remoteStore:_remoteStore
+ initialUser:user];
+
+ _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine];
+
+ // Setup wiring for remote store.
+ _remoteStore.syncEngine = _syncEngine;
+
+ _remoteStore.onlineStateDelegate = _eventManager;
+
+ // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation
+ // queue, etc.) so must be started after LocalStore.
+ [_localStore start];
+ [_remoteStore start];
+}
+
+- (void)userDidChange:(FSTUser *)user {
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ FSTLog(@"User Changed: %@", user);
+ [self.syncEngine userDidChange:user];
+}
+
+- (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion {
+ [self.workerDispatchQueue dispatchAsync:^{
+ self.credentialsProvider.userChangeListener = nil;
+
+ [self.remoteStore shutdown];
+ [self.localStore shutdown];
+ [self.persistence shutdown];
+ if (completion) {
+ [self.userDispatchQueue dispatchAsync:^{
+ completion(nil);
+ }];
+ }
+ }];
+}
+
+- (FSTQueryListener *)listenToQuery:(FSTQuery *)query
+ options:(FSTListenOptions *)options
+ viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler {
+ FSTQueryListener *listener = [[FSTQueryListener alloc] initWithQuery:query
+ options:options
+ viewSnapshotHandler:viewSnapshotHandler];
+
+ [self.workerDispatchQueue dispatchAsync:^{
+ [self.eventManager addListener:listener];
+ }];
+
+ return listener;
+}
+
+- (void)removeListener:(FSTQueryListener *)listener {
+ [self.workerDispatchQueue dispatchAsync:^{
+ [self.eventManager removeListener:listener];
+ }];
+}
+
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations
+ completion:(nullable FSTVoidErrorBlock)completion {
+ [self.workerDispatchQueue dispatchAsync:^{
+ if (mutations.count == 0) {
+ [self.userDispatchQueue dispatchAsync:^{
+ completion(nil);
+ }];
+ } else {
+ [self.syncEngine writeMutations:mutations
+ completion:^(NSError *error) {
+ // Dispatch the result back onto the user dispatch queue.
+ if (completion) {
+ [self.userDispatchQueue dispatchAsync:^{
+ completion(error);
+ }];
+ }
+ }];
+ }
+ }];
+};
+
+- (void)transactionWithRetries:(int)retries
+ updateBlock:(FSTTransactionBlock)updateBlock
+ completion:(FSTVoidIDErrorBlock)completion {
+ [self.workerDispatchQueue dispatchAsync:^{
+ [self.syncEngine transactionWithRetries:retries
+ workerDispatchQueue:self.workerDispatchQueue
+ updateBlock:updateBlock
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ // Dispatch the result back onto the user dispatch queue.
+ if (completion) {
+ [self.userDispatchQueue dispatchAsync:^{
+ completion(result, error);
+ }];
+ }
+ }];
+
+ }];
+}
+
+- (FSTDatabaseID *)databaseID {
+ return self.databaseInfo.databaseID;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTQuery.h b/Firestore/Source/Core/FSTQuery.h
new file mode 100644
index 0000000..0562ae4
--- /dev/null
+++ b/Firestore/Source/Core/FSTQuery.h
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 FSTDocument;
+@class FSTDocumentKey;
+@class FSTFieldPath;
+@class FSTFieldValue;
+@class FSTResourcePath;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTRelationFilterOperator is a value relation operator that can be used to filter documents.
+ * It is similar to NSPredicateOperatorType, but only has operators supported by Firestore.
+ */
+typedef NS_ENUM(NSInteger, FSTRelationFilterOperator) {
+ FSTRelationFilterOperatorLessThan = 0,
+ FSTRelationFilterOperatorLessThanOrEqual,
+ FSTRelationFilterOperatorEqual,
+ FSTRelationFilterOperatorGreaterThanOrEqual,
+ FSTRelationFilterOperatorGreaterThan,
+};
+
+/** Interface used for all query filters. */
+@protocol FSTFilter <NSObject>
+
+/** Returns the field the Filter operates over. */
+- (FSTFieldPath *)field;
+
+/** Returns true if a document matches the filter. */
+- (BOOL)matchesDocument:(FSTDocument *)document;
+
+/** A unique ID identifying the filter; used when serializing queries. */
+- (NSString *)canonicalID;
+
+@end
+
+/**
+ * FSTRelationFilter is a document filter constraint on a query with a single relation operator.
+ * It is similar to NSComparisonPredicate, except customized for Firestore semantics.
+ */
+@interface FSTRelationFilter : NSObject <FSTFilter>
+
+/**
+ * Creates a new constraint for filtering documents.
+ *
+ * @param field A path to a field in the document to filter on. The LHS of the expression.
+ * @param filterOperator The binary operator to apply.
+ * @param value A constant value to compare @a field to. The RHS of the expression.
+ * @return A new instance of FSTRelationFilter.
+ */
++ (instancetype)filterWithField:(FSTFieldPath *)field
+ filterOperator:(FSTRelationFilterOperator)filterOperator
+ value:(FSTFieldValue *)value;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** Returns YES if the receiver is not an equality relation. */
+- (BOOL)isInequality;
+
+/** The left hand side of the relation. A path into a document field. */
+@property(nonatomic, strong, readonly) FSTFieldPath *field;
+
+/** The type of equality/inequality operator to use in the relation. */
+@property(nonatomic, assign, readonly) FSTRelationFilterOperator filterOperator;
+
+/** The right hand side of the relation. A constant value to compare to. */
+@property(nonatomic, strong, readonly) FSTFieldValue *value;
+
+@end
+
+/** Filter that matches NULL values. */
+@interface FSTNullFilter : NSObject <FSTFilter>
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithField:(FSTFieldPath *)field NS_DESIGNATED_INITIALIZER;
+@end
+
+/** Filter that matches NAN values. */
+@interface FSTNanFilter : NSObject <FSTFilter>
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithField:(FSTFieldPath *)field NS_DESIGNATED_INITIALIZER;
+@end
+
+/** FSTSortOrder is a field and direction to order query results by. */
+@interface FSTSortOrder : NSObject <NSCopying>
+
+/** Creates a new sort order with the given field and direction. */
++ (instancetype)sortOrderWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** Compares two documents based on the field and direction of this sort order. */
+- (NSComparisonResult)compareDocument:(FSTDocument *)document1 toDocument:(FSTDocument *)document2;
+
+/** The direction of the sort. */
+@property(nonatomic, assign, readonly, getter=isAscending) BOOL ascending;
+
+/** The field to sort by. */
+@property(nonatomic, strong, readonly) FSTFieldPath *field;
+
+@end
+
+/**
+ * FSTBound represents a bound of a query.
+ *
+ * The bound is specified with the given components representing a position and whether it's just
+ * before or just after the position (relative to whatever the query order is).
+ *
+ * The position represents a logical index position for a query. It's a prefix of values for
+ * the (potentially implicit) order by clauses of a query.
+ *
+ * FSTBound provides a function to determine whether a document comes before or after a bound.
+ * This is influenced by whether the position is just before or just after the provided values.
+ */
+@interface FSTBound : NSObject <NSCopying>
+
+/**
+ * Creates a new bound.
+ *
+ * @param position The position relative to the sort order.
+ * @param isBefore Whether this bound is just before or just after the position.
+ */
++ (instancetype)boundWithPosition:(NSArray<FSTFieldValue *> *)position isBefore:(BOOL)isBefore;
+
+/** Whether this bound is just before or just after the provided position */
+@property(nonatomic, assign, readonly, getter=isBefore) BOOL before;
+
+/** The index position of this bound represented as an array of field values. */
+@property(nonatomic, strong, readonly) NSArray<FSTFieldValue *> *position;
+
+/** Returns YES if a document comes before a bound using the provided sort order. */
+- (BOOL)sortsBeforeDocument:(FSTDocument *)document
+ usingSortOrder:(NSArray<FSTSortOrder *> *)sortOrder;
+
+@end
+
+/** FSTQuery represents the internal structure of a Firestore query. */
+@interface FSTQuery : NSObject <NSCopying>
+
+- (id)init NS_UNAVAILABLE;
+
+/**
+ * Initializes a query with all of its components directly.
+ */
+- (instancetype)initWithPath:(FSTResourcePath *)path
+ filterBy:(NSArray<id<FSTFilter>> *)filters
+ orderBy:(NSArray<FSTSortOrder *> *)sortOrders
+ limit:(NSInteger)limit
+ startAt:(nullable FSTBound *)startAtBound
+ endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Creates and returns a new FSTQuery.
+ *
+ * @param path The path to the collection to be queried over.
+ * @return A new instance of FSTQuery.
+ */
++ (instancetype)queryWithPath:(FSTResourcePath *)path;
+
+/**
+ * Returns the list of ordering constraints that were explicitly requested on the query by the
+ * user.
+ *
+ * Note that the actual query performed might add additional sort orders to match the behavior
+ * of the backend.
+ */
+- (NSArray<FSTSortOrder *> *)explicitSortOrders;
+
+/**
+ * Returns the full list of ordering constraints on the query.
+ *
+ * This might include additional sort orders added implicitly to match the backend behavior.
+ */
+- (NSArray<FSTSortOrder *> *)sortOrders;
+
+/**
+ * Creates a new FSTQuery with an additional filter.
+ *
+ * @param filter The predicate to filter by.
+ * @return the new FSTQuery.
+ */
+- (instancetype)queryByAddingFilter:(id<FSTFilter>)filter;
+
+/**
+ * Creates a new FSTQuery with an additional ordering constraint.
+ *
+ * @param sortOrder The key and direction to order by.
+ * @return the new FSTQuery.
+ */
+- (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder;
+
+/**
+ * Returns a new FSTQuery with the given limit on how many results can be returned.
+ *
+ * @param limit The maximum number of results to return. If @a limit <= 0, behavior is unspecified.
+ * If @a limit == NSNotFound, then no limit is applied.
+ */
+- (instancetype)queryBySettingLimit:(NSInteger)limit;
+
+/**
+ * Creates a new FSTQuery starting at the provided bound.
+ *
+ * @param bound The bound to start this query at.
+ * @return the new FSTQuery.
+ */
+- (instancetype)queryByAddingStartAt:(FSTBound *)bound;
+
+/**
+ * Creates a new FSTQuery ending at the provided bound.
+ *
+ * @param bound The bound to end this query at.
+ * @return the new FSTQuery.
+ */
+- (instancetype)queryByAddingEndAt:(FSTBound *)bound;
+
+/** Returns YES if the receiver is query for a specific document. */
+- (BOOL)isDocumentQuery;
+
+/** Returns YES if the @a document matches the constraints of the receiver. */
+- (BOOL)matchesDocument:(FSTDocument *)document;
+
+/** Returns a comparator that will sort documents according to the receiver's sort order. */
+- (NSComparator)comparator;
+
+/** Returns the field of the first filter on the receiver that's an inequality, or nil if none. */
+- (FSTFieldPath *_Nullable)inequalityFilterField;
+
+/** Returns the first field in an order-by constraint, or nil if none. */
+- (FSTFieldPath *_Nullable)firstSortOrderField;
+
+/** The base path of the query. */
+@property(nonatomic, strong, readonly) FSTResourcePath *path;
+
+/** The filters on the documents returned by the query. */
+@property(nonatomic, strong, readonly) NSArray<id<FSTFilter>> *filters;
+
+/** The maximum number of results to return, or NSNotFound if no limit. */
+@property(nonatomic, assign, readonly) NSInteger limit;
+
+/**
+ * A canonical string identifying the query. Two different instances of equivalent queries will
+ * return the same canonicalID.
+ */
+@property(nonatomic, strong, readonly) NSString *canonicalID;
+
+/** An optional bound to start the query at. */
+@property(nonatomic, nullable, strong, readonly) FSTBound *startAt;
+
+/** An optional bound to end the query at. */
+@property(nonatomic, nullable, strong, readonly) FSTBound *endAt;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTQuery.m b/Firestore/Source/Core/FSTQuery.m
new file mode 100644
index 0000000..b220c7c
--- /dev/null
+++ b/Firestore/Source/Core/FSTQuery.m
@@ -0,0 +1,759 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTQuery.h"
+
+#import "FIRFirestore+Internal.h"
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTFieldValue.h"
+#import "FSTPath.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTRelationFilterOperator functions
+
+NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOperator) {
+ switch (filterOperator) {
+ case FSTRelationFilterOperatorLessThan:
+ return @"<";
+ case FSTRelationFilterOperatorLessThanOrEqual:
+ return @"<=";
+ case FSTRelationFilterOperatorEqual:
+ return @"==";
+ case FSTRelationFilterOperatorGreaterThanOrEqual:
+ return @">=";
+ case FSTRelationFilterOperatorGreaterThan:
+ return @">";
+ default:
+ FSTCFail(@"Unknown FSTRelationFilterOperator %lu", (unsigned long)filterOperator);
+ }
+}
+
+#pragma mark - FSTRelationFilter
+
+@interface FSTRelationFilter ()
+
+/**
+ * Initializes the receiver relation filter.
+ *
+ * @param field A path to a field in the document to filter on. The LHS of the expression.
+ * @param filterOperator The binary operator to apply.
+ * @param value A constant value to compare @a field to. The RHS of the expression.
+ */
+- (instancetype)initWithField:(FSTFieldPath *)field
+ filterOperator:(FSTRelationFilterOperator)filterOperator
+ value:(FSTFieldValue *)value NS_DESIGNATED_INITIALIZER;
+
+/** Returns YES if @a document matches the receiver's constraint. */
+- (BOOL)matchesDocument:(FSTDocument *)document;
+
+/**
+ * A canonical string identifying the filter. Two different instances of equivalent filters will
+ * return the same canonicalID.
+ */
+- (NSString *)canonicalID;
+
+@end
+
+@implementation FSTRelationFilter
+
+#pragma mark - Constructor methods
+
++ (instancetype)filterWithField:(FSTFieldPath *)field
+ filterOperator:(FSTRelationFilterOperator)filterOperator
+ value:(FSTFieldValue *)value {
+ return [[FSTRelationFilter alloc] initWithField:field filterOperator:filterOperator value:value];
+}
+
+- (instancetype)initWithField:(FSTFieldPath *)field
+ filterOperator:(FSTRelationFilterOperator)filterOperator
+ value:(FSTFieldValue *)value {
+ self = [super init];
+ if (self) {
+ _field = field;
+ _filterOperator = filterOperator;
+ _value = value;
+ }
+ return self;
+}
+
+#pragma mark - Public Methods
+
+- (BOOL)isInequality {
+ return self.filterOperator != FSTRelationFilterOperatorEqual;
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %@ %@", [self.field canonicalString],
+ FSTStringFromQueryRelationOperator(self.filterOperator),
+ self.value];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTRelationFilter class]]) {
+ return NO;
+ }
+ return [self isEqualToFilter:(FSTRelationFilter *)other];
+}
+
+#pragma mark - Private methods
+
+- (BOOL)matchesDocument:(FSTDocument *)document {
+ if ([self.field isKeyFieldPath]) {
+ FSTAssert([self.value isKindOfClass:[FSTReferenceValue class]],
+ @"Comparing on key, but filter value not a FSTReferenceValue.");
+ FSTReferenceValue *refValue = (FSTReferenceValue *)self.value;
+ NSComparisonResult comparison = FSTDocumentKeyComparator(document.key, refValue.value);
+ return [self matchesComparison:comparison];
+ } else {
+ return [self matchesValue:[document fieldForPath:self.field]];
+ }
+}
+
+- (NSString *)canonicalID {
+ // TODO(b/37283291): This should be collision robust and avoid relying on |description| methods.
+ return [NSString stringWithFormat:@"%@%@%@", [self.field canonicalString],
+ FSTStringFromQueryRelationOperator(self.filterOperator),
+ [self.value value]];
+}
+
+- (BOOL)isEqualToFilter:(FSTRelationFilter *)other {
+ if (self.filterOperator != other.filterOperator) {
+ return NO;
+ }
+ if (![self.field isEqual:other.field]) {
+ return NO;
+ }
+ if (![self.value isEqual:other.value]) {
+ return NO;
+ }
+ return YES;
+}
+
+/** Returns YES if receiver is true with the given value as its LHS. */
+- (BOOL)matchesValue:(FSTFieldValue *)other {
+ // Only compare types with matching backend order (such as double and int).
+ return self.value.typeOrder == other.typeOrder &&
+ [self matchesComparison:[other compare:self.value]];
+}
+
+- (BOOL)matchesComparison:(NSComparisonResult)comparison {
+ switch (self.filterOperator) {
+ case FSTRelationFilterOperatorLessThan:
+ return comparison == NSOrderedAscending;
+ case FSTRelationFilterOperatorLessThanOrEqual:
+ return comparison == NSOrderedAscending || comparison == NSOrderedSame;
+ case FSTRelationFilterOperatorEqual:
+ return comparison == NSOrderedSame;
+ case FSTRelationFilterOperatorGreaterThanOrEqual:
+ return comparison == NSOrderedDescending || comparison == NSOrderedSame;
+ case FSTRelationFilterOperatorGreaterThan:
+ return comparison == NSOrderedDescending;
+ default:
+ FSTFail(@"Unknown operator: %ld", (long)self.filterOperator);
+ }
+}
+
+@end
+
+#pragma mark - FSTNullFilter
+
+@interface FSTNullFilter ()
+@property(nonatomic, strong, readonly) FSTFieldPath *field;
+@end
+
+@implementation FSTNullFilter
+- (instancetype)initWithField:(FSTFieldPath *)field {
+ if (self = [super init]) {
+ _field = field;
+ }
+ return self;
+}
+
+- (BOOL)matchesDocument:(FSTDocument *)document {
+ FSTFieldValue *fieldValue = [document fieldForPath:self.field];
+ return fieldValue != nil && [fieldValue isEqual:[FSTNullValue nullValue]];
+}
+
+- (NSString *)canonicalID {
+ return [NSString stringWithFormat:@"%@ IS NULL", [self.field canonicalString]];
+}
+
+- (NSString *)description {
+ return [self canonicalID];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (!other || ![[other class] isEqual:[self class]]) return NO;
+
+ return [self.field isEqual:((FSTNullFilter *)other).field];
+}
+
+- (NSUInteger)hash {
+ return [self.field hash];
+}
+
+@end
+
+#pragma mark - FSTNanFilter
+
+@interface FSTNanFilter ()
+@property(nonatomic, strong, readonly) FSTFieldPath *field;
+@end
+
+@implementation FSTNanFilter
+
+- (instancetype)initWithField:(FSTFieldPath *)field {
+ if (self = [super init]) {
+ _field = field;
+ }
+ return self;
+}
+
+- (BOOL)matchesDocument:(FSTDocument *)document {
+ FSTFieldValue *fieldValue = [document fieldForPath:self.field];
+ return fieldValue != nil && [fieldValue isEqual:[FSTDoubleValue nanValue]];
+}
+
+- (NSString *)canonicalID {
+ return [NSString stringWithFormat:@"%@ IS NaN", [self.field canonicalString]];
+}
+
+- (NSString *)description {
+ return [self canonicalID];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (!other || ![[other class] isEqual:[self class]]) return NO;
+
+ return [self.field isEqual:((FSTNanFilter *)other).field];
+}
+
+- (NSUInteger)hash {
+ return [self.field hash];
+}
+@end
+
+#pragma mark - FSTSortOrder
+
+@interface FSTSortOrder ()
+
+/** Creates a new sort order with the given field and direction. */
+- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending;
+
+- (NSString *)canonicalID;
+
+@end
+
+@implementation FSTSortOrder
+
+#pragma mark - Constructor methods
+
++ (instancetype)sortOrderWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending {
+ return [[FSTSortOrder alloc] initWithFieldPath:fieldPath ascending:ascending];
+}
+
+- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending {
+ self = [super init];
+ if (self) {
+ _field = fieldPath;
+ _ascending = ascending;
+ }
+ return self;
+}
+
+#pragma mark - Public methods
+
+- (NSComparisonResult)compareDocument:(FSTDocument *)document1 toDocument:(FSTDocument *)document2 {
+ int modifier = self.isAscending ? 1 : -1;
+ if ([self.field isEqual:[FSTFieldPath keyFieldPath]]) {
+ return (NSComparisonResult)(modifier * FSTDocumentKeyComparator(document1.key, document2.key));
+ } else {
+ FSTFieldValue *value1 = [document1 fieldForPath:self.field];
+ FSTFieldValue *value2 = [document2 fieldForPath:self.field];
+ FSTAssert(value1 != nil && value2 != nil,
+ @"Trying to compare documents on fields that don't exist.");
+ return modifier * [value1 compare:value2];
+ }
+}
+
+- (NSString *)canonicalID {
+ return [NSString
+ stringWithFormat:@"%@%@", self.field.canonicalString, self.isAscending ? @"asc" : @"desc"];
+}
+
+- (BOOL)isEqualToSortOrder:(FSTSortOrder *)other {
+ return [self.field isEqual:other.field] && self.isAscending == other.isAscending;
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTSortOrder: path:%@ dir:%@>", self.field,
+ self.ascending ? @"asc" : @"desc"];
+}
+
+- (BOOL)isEqual:(NSObject *)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTSortOrder class]]) {
+ return NO;
+ }
+ return [self isEqualToSortOrder:(FSTSortOrder *)other];
+}
+
+- (NSUInteger)hash {
+ return [self.canonicalID hash];
+}
+
+- (instancetype)copyWithZone:(nullable NSZone *)zone {
+ return self;
+}
+
+@end
+
+#pragma mark - FSTBound
+
+@implementation FSTBound
+
+- (instancetype)initWithPosition:(NSArray<FSTFieldValue *> *)position isBefore:(BOOL)isBefore {
+ if (self = [super init]) {
+ _position = position;
+ _before = isBefore;
+ }
+ return self;
+}
+
++ (instancetype)boundWithPosition:(NSArray<FSTFieldValue *> *)position isBefore:(BOOL)isBefore {
+ return [[FSTBound alloc] initWithPosition:position isBefore:isBefore];
+}
+
+- (NSString *)canonicalString {
+ // TODO(b/29183165): Make this collision robust.
+ NSMutableString *string = [NSMutableString string];
+ if (self.isBefore) {
+ [string appendString:@"b:"];
+ } else {
+ [string appendString:@"a:"];
+ }
+ for (FSTFieldValue *component in self.position) {
+ [string appendFormat:@"%@", component];
+ }
+ return string;
+}
+
+- (BOOL)sortsBeforeDocument:(FSTDocument *)document
+ usingSortOrder:(NSArray<FSTSortOrder *> *)sortOrder {
+ FSTAssert(self.position.count <= sortOrder.count,
+ @"FSTIndexPosition has more components than provided sort order.");
+ __block NSComparisonResult result = NSOrderedSame;
+ [self.position enumerateObjectsUsingBlock:^(FSTFieldValue *fieldValue, NSUInteger idx,
+ BOOL *stop) {
+ FSTSortOrder *sortOrderComponent = sortOrder[idx];
+ NSComparisonResult comparison;
+ if ([sortOrderComponent.field isEqual:[FSTFieldPath keyFieldPath]]) {
+ FSTAssert([fieldValue isKindOfClass:[FSTReferenceValue class]],
+ @"FSTBound has a non-key value where the key path is being used %@", fieldValue);
+ comparison = [fieldValue.value compare:document.key];
+ } else {
+ FSTFieldValue *docValue = [document fieldForPath:sortOrderComponent.field];
+ FSTAssert(docValue != nil, @"Field should exist since document matched the orderBy already.");
+ comparison = [fieldValue compare:docValue];
+ }
+
+ if (!sortOrderComponent.isAscending) {
+ comparison = comparison * -1;
+ }
+
+ if (comparison != 0) {
+ result = comparison;
+ *stop = YES;
+ }
+ }];
+
+ return self.isBefore ? result <= NSOrderedSame : result < NSOrderedSame;
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTBound: position:%@ before:%@>", self.position,
+ self.isBefore ? @"YES" : @"NO"];
+}
+
+- (BOOL)isEqual:(NSObject *)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTBound class]]) {
+ return NO;
+ }
+
+ FSTBound *otherBound = (FSTBound *)other;
+
+ return [self.position isEqualToArray:otherBound.position] && self.isBefore == otherBound.isBefore;
+}
+
+- (NSUInteger)hash {
+ return 31 * self.position.hash + (self.isBefore ? 0 : 1);
+}
+
+- (instancetype)copyWithZone:(nullable NSZone *)zone {
+ return self;
+}
+
+@end
+
+#pragma mark - FSTQuery
+
+@interface FSTQuery () {
+ // Cached value of the canonicalID property.
+ NSString *_canonicalID;
+}
+
+/**
+ * Initializes the receiver with the given query constraints.
+ *
+ * @param path The base path of the query.
+ * @param filters Filters specify which documents to include in the results.
+ * @param sortOrders The fields and directions to sort the results.
+ * @param limit If not NSNotFound, only this many results will be returned.
+ */
+- (instancetype)initWithPath:(FSTResourcePath *)path
+ filterBy:(NSArray<id<FSTFilter>> *)filters
+ orderBy:(NSArray<FSTSortOrder *> *)sortOrders
+ limit:(NSInteger)limit
+ startAt:(nullable FSTBound *)startAtBound
+ endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER;
+
+/** A list of fields given to sort by. This does not include the implicit key sort at the end. */
+@property(nonatomic, strong, readonly) NSArray<FSTSortOrder *> *explicitSortOrders;
+
+/** The memoized list of sort orders */
+@property(nonatomic, nullable, strong, readwrite) NSArray<FSTSortOrder *> *memoizedSortOrders;
+
+@end
+
+@implementation FSTQuery
+
+#pragma mark - Constructors
+
++ (instancetype)queryWithPath:(FSTResourcePath *)path {
+ return [[FSTQuery alloc] initWithPath:path
+ filterBy:@[]
+ orderBy:@[]
+ limit:NSNotFound
+ startAt:nil
+ endAt:nil];
+}
+
+- (instancetype)initWithPath:(FSTResourcePath *)path
+ filterBy:(NSArray<id<FSTFilter>> *)filters
+ orderBy:(NSArray<FSTSortOrder *> *)sortOrders
+ limit:(NSInteger)limit
+ startAt:(nullable FSTBound *)startAtBound
+ endAt:(nullable FSTBound *)endAtBound {
+ if (self = [super init]) {
+ _path = path;
+ _filters = filters;
+ _explicitSortOrders = sortOrders;
+ _limit = limit;
+ _startAt = startAtBound;
+ _endAt = endAtBound;
+ }
+ return self;
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTQuery: canonicalID:%@>", self.canonicalID];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+ if (![object isKindOfClass:[FSTQuery class]]) {
+ return NO;
+ }
+ return [self isEqualToQuery:(FSTQuery *)object];
+}
+
+- (NSUInteger)hash {
+ return [self.canonicalID hash];
+}
+
+- (instancetype)copyWithZone:(nullable NSZone *)zone {
+ return self;
+}
+
+#pragma mark - Public methods
+
+- (NSArray *)sortOrders {
+ if (self.memoizedSortOrders == nil) {
+ FSTFieldPath *_Nullable inequalityField = [self inequalityFilterField];
+ FSTFieldPath *_Nullable firstSortOrderField = [self firstSortOrderField];
+ if (inequalityField && !firstSortOrderField) {
+ // In order to implicitly add key ordering, we must also add the inequality filter field for
+ // it to be a valid query. Note that the default inequality field and key ordering is
+ // ascending.
+ if ([inequalityField isKeyFieldPath]) {
+ self.memoizedSortOrders =
+ @[ [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] ];
+ } else {
+ self.memoizedSortOrders = @[
+ [FSTSortOrder sortOrderWithFieldPath:inequalityField ascending:YES],
+ [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES]
+ ];
+ }
+ } else {
+ FSTAssert(!inequalityField || [inequalityField isEqual:firstSortOrderField],
+ @"First orderBy %@ should match inequality field %@.", firstSortOrderField,
+ inequalityField);
+
+ __block BOOL foundKeyOrder = NO;
+
+ NSMutableArray *result = [NSMutableArray array];
+ for (FSTSortOrder *sortOrder in self.explicitSortOrders) {
+ [result addObject:sortOrder];
+ if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) {
+ foundKeyOrder = YES;
+ }
+ }
+
+ if (!foundKeyOrder) {
+ // The direction of the implicit key ordering always matches the direction of the last
+ // explicit sort order
+ BOOL lastIsAscending =
+ self.explicitSortOrders.count > 0 ? self.explicitSortOrders.lastObject.ascending : YES;
+ [result addObject:[FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath]
+ ascending:lastIsAscending]];
+ }
+
+ self.memoizedSortOrders = result;
+ }
+ }
+ return self.memoizedSortOrders;
+}
+
+- (instancetype)queryByAddingFilter:(id<FSTFilter>)filter {
+ FSTAssert(![FSTDocumentKey isDocumentKey:self.path], @"No filtering allowed for document query");
+
+ FSTFieldPath *_Nullable newInequalityField = nil;
+ if ([filter isKindOfClass:[FSTRelationFilter class]] &&
+ [((FSTRelationFilter *)filter)isInequality]) {
+ newInequalityField = filter.field;
+ }
+ FSTFieldPath *_Nullable queryInequalityField = [self inequalityFilterField];
+ FSTAssert(!queryInequalityField || !newInequalityField ||
+ [queryInequalityField isEqual:newInequalityField],
+ @"Query must only have one inequality field.");
+
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:[self.filters arrayByAddingObject:filter]
+ orderBy:self.explicitSortOrders
+ limit:self.limit
+ startAt:self.startAt
+ endAt:self.endAt];
+}
+
+- (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder {
+ FSTAssert(![FSTDocumentKey isDocumentKey:self.path],
+ @"No ordering is allowed for a document query.");
+
+ // TODO(klimt): Validate that the same key isn't added twice.
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:self.filters
+ orderBy:[self.explicitSortOrders arrayByAddingObject:sortOrder]
+ limit:self.limit
+ startAt:self.startAt
+ endAt:self.endAt];
+}
+
+- (instancetype)queryBySettingLimit:(NSInteger)limit {
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:self.filters
+ orderBy:self.explicitSortOrders
+ limit:limit
+ startAt:self.startAt
+ endAt:self.endAt];
+}
+
+- (instancetype)queryByAddingStartAt:(FSTBound *)bound {
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:self.filters
+ orderBy:self.explicitSortOrders
+ limit:self.limit
+ startAt:bound
+ endAt:self.endAt];
+}
+
+- (instancetype)queryByAddingEndAt:(FSTBound *)bound {
+ return [[FSTQuery alloc] initWithPath:self.path
+ filterBy:self.filters
+ orderBy:self.explicitSortOrders
+ limit:self.limit
+ startAt:self.startAt
+ endAt:bound];
+}
+
+- (BOOL)isDocumentQuery {
+ return [FSTDocumentKey isDocumentKey:self.path] && self.filters.count == 0;
+}
+
+- (BOOL)matchesDocument:(FSTDocument *)document {
+ return [self pathMatchesDocument:document] && [self orderByMatchesDocument:document] &&
+ [self filtersMatchDocument:document] && [self boundsMatchDocument:document];
+}
+
+- (NSComparator)comparator {
+ return ^NSComparisonResult(id document1, id document2) {
+ BOOL didCompareOnKeyField = NO;
+ for (FSTSortOrder *orderBy in self.sortOrders) {
+ NSComparisonResult comp = [orderBy compareDocument:document1 toDocument:document2];
+ if (comp != NSOrderedSame) {
+ return comp;
+ }
+ didCompareOnKeyField =
+ didCompareOnKeyField || [orderBy.field isEqual:[FSTFieldPath keyFieldPath]];
+ }
+ FSTAssert(didCompareOnKeyField, @"sortOrder of query did not include key ordering");
+ return NSOrderedSame;
+ };
+}
+
+- (FSTFieldPath *_Nullable)inequalityFilterField {
+ for (id<FSTFilter> filter in self.filters) {
+ if ([filter isKindOfClass:[FSTRelationFilter class]] &&
+ ((FSTRelationFilter *)filter).filterOperator != FSTRelationFilterOperatorEqual) {
+ return filter.field;
+ }
+ }
+ return nil;
+}
+
+- (FSTFieldPath *_Nullable)firstSortOrderField {
+ return self.explicitSortOrders.firstObject.field;
+}
+
+#pragma mark - Private properties
+
+- (NSString *)canonicalID {
+ if (_canonicalID) {
+ return _canonicalID;
+ }
+
+ NSMutableString *canonicalID = [[self.path canonicalString] mutableCopy];
+
+ // Add filters.
+ [canonicalID appendString:@"|f:"];
+ for (id<FSTFilter> predicate in self.filters) {
+ [canonicalID appendFormat:@"%@", [predicate canonicalID]];
+ }
+
+ // Add order by.
+ [canonicalID appendString:@"|ob:"];
+ for (FSTSortOrder *orderBy in self.sortOrders) {
+ [canonicalID appendString:orderBy.canonicalID];
+ }
+
+ // Add limit.
+ if (self.limit != NSNotFound) {
+ [canonicalID appendFormat:@"|l:%ld", (long)self.limit];
+ }
+
+ if (self.startAt) {
+ [canonicalID appendFormat:@"|lb:%@", self.startAt.canonicalString];
+ }
+
+ if (self.endAt) {
+ [canonicalID appendFormat:@"|ub:%@", self.endAt.canonicalString];
+ }
+
+ _canonicalID = canonicalID;
+ return canonicalID;
+}
+
+#pragma mark - Private methods
+
+- (BOOL)isEqualToQuery:(FSTQuery *)other {
+ return [self.path isEqual:other.path] && self.limit == other.limit &&
+ [self.filters isEqual:other.filters] && [self.sortOrders isEqual:other.sortOrders] &&
+ (self.startAt == other.startAt || [self.startAt isEqual:other.startAt]) &&
+ (self.endAt == other.endAt || [self.endAt isEqual:other.endAt]);
+}
+
+/* Returns YES if the document matches the path for the receiver. */
+- (BOOL)pathMatchesDocument:(FSTDocument *)document {
+ FSTResourcePath *documentPath = document.key.path;
+ if ([FSTDocumentKey isDocumentKey:self.path]) {
+ // Exact match for document queries.
+ return [self.path isEqual:documentPath];
+ } else {
+ // Shallow ancestor queries by default.
+ return [self.path isPrefixOfPath:documentPath] && self.path.length == documentPath.length - 1;
+ }
+}
+
+/**
+ * A document must have a value for every ordering clause in order to show up in the results.
+ */
+- (BOOL)orderByMatchesDocument:(FSTDocument *)document {
+ for (FSTSortOrder *orderBy in self.explicitSortOrders) {
+ FSTFieldPath *fieldPath = orderBy.field;
+ // order by key always matches
+ if (![fieldPath isEqual:[FSTFieldPath keyFieldPath]] &&
+ [document fieldForPath:fieldPath] == nil) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+/** Returns YES if the document matches all of the filters in the receiver. */
+- (BOOL)filtersMatchDocument:(FSTDocument *)document {
+ for (id<FSTFilter> filter in self.filters) {
+ if (![filter matchesDocument:document]) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+- (BOOL)boundsMatchDocument:(FSTDocument *)document {
+ if (self.startAt && ![self.startAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) {
+ return NO;
+ }
+ if (self.endAt && [self.endAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) {
+ return NO;
+ }
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTSnapshotVersion.h b/Firestore/Source/Core/FSTSnapshotVersion.h
new file mode 100644
index 0000000..b72e4a2
--- /dev/null
+++ b/Firestore/Source/Core/FSTSnapshotVersion.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FSTTimestamp;
+
+/**
+ * A version of a document in Firestore. This corresponds to the version timestamp, such as
+ * update_time or read_time.
+ */
+@interface FSTSnapshotVersion : NSObject <NSCopying>
+
+/** Creates a new version that is smaller than all other versions. */
++ (instancetype)noVersion;
+
+/** Creates a new version representing the given timestamp. */
++ (instancetype)versionWithTimestamp:(FSTTimestamp *)timestamp;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (NSComparisonResult)compare:(FSTSnapshotVersion *)other;
+
+@property(nonatomic, strong, readonly) FSTTimestamp *timestamp;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTSnapshotVersion.m b/Firestore/Source/Core/FSTSnapshotVersion.m
new file mode 100644
index 0000000..68d5d7f
--- /dev/null
+++ b/Firestore/Source/Core/FSTSnapshotVersion.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 "FSTSnapshotVersion.h"
+
+#import "FSTTimestamp.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTSnapshotVersion
+
++ (instancetype)noVersion {
+ static FSTSnapshotVersion *min;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:0];
+ min = [FSTSnapshotVersion versionWithTimestamp:timestamp];
+ });
+ return min;
+}
+
++ (instancetype)versionWithTimestamp:(FSTTimestamp *)timestamp {
+ return [[FSTSnapshotVersion alloc] initWithTimestamp:timestamp];
+}
+
+- (instancetype)initWithTimestamp:(FSTTimestamp *)timestamp {
+ self = [super init];
+ if (self) {
+ _timestamp = timestamp;
+ }
+ return self;
+}
+
+#pragma mark - NSObject methods
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+ if (![object isKindOfClass:[FSTSnapshotVersion class]]) {
+ return NO;
+ }
+ return [self.timestamp isEqual:((FSTSnapshotVersion *)object).timestamp];
+}
+
+- (NSUInteger)hash {
+ return self.timestamp.hash;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTSnapshotVersion: %@>", self.timestamp];
+}
+
+- (id)copyWithZone:(NSZone *_Nullable)zone {
+ // Implements NSCopying without actually copying because timestamps are immutable.
+ return self;
+}
+
+#pragma mark - Public methods
+
+- (NSComparisonResult)compare:(FSTSnapshotVersion *)other {
+ return [self.timestamp compare:other.timestamp];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTSyncEngine.h b/Firestore/Source/Core/FSTSyncEngine.h
new file mode 100644
index 0000000..1348ce1
--- /dev/null
+++ b/Firestore/Source/Core/FSTSyncEngine.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTRemoteStore.h"
+#import "FSTTypes.h"
+
+@class FSTDispatchQueue;
+@class FSTLocalStore;
+@class FSTMutation;
+@class FSTQuery;
+@class FSTRemoteEvent;
+@class FSTRemoteStore;
+@class FSTUser;
+@class FSTViewSnapshot;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTSyncEngineDelegate
+
+/** A Delegate to be notified when the sync engine produces new view snapshots or errors. */
+@protocol FSTSyncEngineDelegate
+- (void)handleViewSnapshots:(NSArray<FSTViewSnapshot *> *)viewSnapshots;
+- (void)handleError:(NSError *)error forQuery:(FSTQuery *)query;
+@end
+
+/**
+ * SyncEngine is the central controller in the client SDK architecture. It is the glue code
+ * between the EventManager, LocalStore, and RemoteStore. Some of SyncEngine's responsibilities
+ * include:
+ * 1. Coordinating client requests and remote events between the EventManager and the local and
+ * remote data stores.
+ * 2. Managing a View object for each query, providing the unified view between the local and
+ * remote data stores.
+ * 3. Notifying the RemoteStore when the LocalStore has new mutations in its queue that need
+ * sending to the backend.
+ *
+ * The SyncEngine’s methods should only ever be called by methods running on our own worker
+ * dispatch queue.
+ */
+@interface FSTSyncEngine : NSObject <FSTRemoteSyncer>
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore
+ remoteStore:(FSTRemoteStore *)remoteStore
+ initialUser:(FSTUser *)user NS_DESIGNATED_INITIALIZER;
+
+/**
+ * A delegate to be notified when queries being listened to produce new view snapshots or
+ * errors.
+ */
+@property(nonatomic, weak) id<FSTSyncEngineDelegate> delegate;
+
+/**
+ * Initiates a new listen. The FSTLocalStore will be queried for initial data and the listen will
+ * be sent to the FSTRemoteStore to get remote data. The registered FSTSyncEngineDelegate will be
+ * notified of resulting view snapshots and/or listen errors.
+ *
+ * @return the target ID assigned to the query.
+ */
+- (FSTTargetID)listenToQuery:(FSTQuery *)query;
+
+/** Stops listening to a query previously listened to via listenToQuery:. */
+- (void)stopListeningToQuery:(FSTQuery *)query;
+
+/**
+ * Initiates the write of local mutation batch which involves adding the writes to the mutation
+ * queue, notifying the remote store about new mutations, and raising events for any changes this
+ * write caused. The provided completion block will be called once the write has been acked or
+ * rejected by the backend (or failed locally for any other reason).
+ */
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations completion:(FSTVoidErrorBlock)completion;
+
+/**
+ * Runs the given transaction block up to retries times and then calls completion.
+ *
+ * @param retries The number of times to try before giving up.
+ * @param workerDispatchQueue The queue to dispatch sync engine calls to.
+ * @param updateBlock The block to call to execute the user's transaction.
+ * @param completion The block to call when the transaction is finished or failed.
+ */
+- (void)transactionWithRetries:(int)retries
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ updateBlock:(FSTTransactionBlock)updateBlock
+ completion:(FSTVoidIDErrorBlock)completion;
+
+- (void)userDidChange:(FSTUser *)user;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTSyncEngine.m b/Firestore/Source/Core/FSTSyncEngine.m
new file mode 100644
index 0000000..8698a97
--- /dev/null
+++ b/Firestore/Source/Core/FSTSyncEngine.m
@@ -0,0 +1,520 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTSyncEngine.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+#import "FIRFirestoreErrors.h"
+#import "FSTAssert.h"
+#import "FSTDispatchQueue.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTDocumentSet.h"
+#import "FSTEagerGarbageCollector.h"
+#import "FSTLocalStore.h"
+#import "FSTLocalViewChanges.h"
+#import "FSTLocalWriteResult.h"
+#import "FSTLogger.h"
+#import "FSTMutationBatch.h"
+#import "FSTQuery.h"
+#import "FSTQueryData.h"
+#import "FSTReferenceSet.h"
+#import "FSTRemoteEvent.h"
+#import "FSTSnapshotVersion.h"
+#import "FSTTargetIDGenerator.h"
+#import "FSTTransaction.h"
+#import "FSTUser.h"
+#import "FSTView.h"
+#import "FSTViewSnapshot.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTQueryView
+
+/**
+ * FSTQueryView contains all of the info that FSTSyncEngine needs to track for a particular
+ * query and view.
+ */
+@interface FSTQueryView : NSObject
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ targetID:(FSTTargetID)targetID
+ resumeToken:(NSData *)resumeToken
+ view:(FSTView *)view NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** The query itself. */
+@property(nonatomic, strong, readonly) FSTQuery *query;
+
+/** The targetID created by the client that is used in the watch stream to identify this query. */
+@property(nonatomic, assign, readonly) FSTTargetID targetID;
+
+/**
+ * An identifier from the datastore backend that indicates the last state of the results that
+ * was received. This can be used to indicate where to continue receiving new doc changes for the
+ * query.
+ */
+@property(nonatomic, copy, readonly) NSData *resumeToken;
+
+/**
+ * The view is responsible for computing the final merged truth of what docs are in the query.
+ * It gets notified of local and remote changes, and applies the query filters and limits to
+ * determine the most correct possible results.
+ */
+@property(nonatomic, strong, readonly) FSTView *view;
+
+@end
+
+@implementation FSTQueryView
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ targetID:(FSTTargetID)targetID
+ resumeToken:(NSData *)resumeToken
+ view:(FSTView *)view {
+ if (self = [super init]) {
+ _query = query;
+ _targetID = targetID;
+ _resumeToken = resumeToken;
+ _view = view;
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - FSTSyncEngine
+
+@interface FSTSyncEngine ()
+
+/** The local store, used to persist mutations and cached documents. */
+@property(nonatomic, strong, readonly) FSTLocalStore *localStore;
+
+/** The remote store for sending writes, watches, etc. to the backend. */
+@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore;
+
+/** FSTQueryViews for all active queries, indexed by query. */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTQuery *, FSTQueryView *> *queryViewsByQuery;
+
+/** FSTQueryViews for all active queries, indexed by target ID. */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<NSNumber *, FSTQueryView *> *queryViewsByTarget;
+
+/**
+ * When a document is in limbo, we create a special listen to resolve it. This maps the
+ * FSTDocumentKey of each limbo document to the FSTTargetID of the listen resolving it.
+ */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *limboTargetsByKey;
+
+/** The inverse of limboTargetsByKey, a map of FSTTargetID to the key of the limbo doc. */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTBoxedTargetID *, FSTDocumentKey *> *limboKeysByTarget;
+
+/** Used to track any documents that are currently in limbo. */
+@property(nonatomic, strong, readonly) FSTReferenceSet *limboDocumentRefs;
+
+/** The garbage collector used to collect documents that are no longer in limbo. */
+@property(nonatomic, strong, readonly) FSTEagerGarbageCollector *limboCollector;
+
+/** Stores user completion blocks, indexed by user and FSTBatchID. */
+@property(nonatomic, strong)
+ NSMutableDictionary<FSTUser *, NSMutableDictionary<NSNumber *, FSTVoidErrorBlock> *>
+ *mutationCompletionBlocks;
+
+/** Used for creating the FSTTargetIDs for the listens used to resolve limbo documents. */
+@property(nonatomic, strong, readonly) FSTTargetIDGenerator *targetIdGenerator;
+
+@property(nonatomic, strong) FSTUser *currentUser;
+
+@end
+
+@implementation FSTSyncEngine
+
+- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore
+ remoteStore:(FSTRemoteStore *)remoteStore
+ initialUser:(FSTUser *)initialUser {
+ if (self = [super init]) {
+ _localStore = localStore;
+ _remoteStore = remoteStore;
+
+ _queryViewsByQuery = [NSMutableDictionary dictionary];
+ _queryViewsByTarget = [NSMutableDictionary dictionary];
+
+ _limboTargetsByKey = [NSMutableDictionary dictionary];
+ _limboKeysByTarget = [NSMutableDictionary dictionary];
+ _limboCollector = [[FSTEagerGarbageCollector alloc] init];
+ _limboDocumentRefs = [[FSTReferenceSet alloc] init];
+ [_limboCollector addGarbageSource:_limboDocumentRefs];
+
+ _mutationCompletionBlocks = [NSMutableDictionary dictionary];
+ _targetIdGenerator = [FSTTargetIDGenerator generatorForSyncEngineStartingAfterID:0];
+ _currentUser = initialUser;
+ }
+ return self;
+}
+
+- (FSTTargetID)listenToQuery:(FSTQuery *)query {
+ [self assertDelegateExistsForSelector:_cmd];
+ FSTAssert(self.queryViewsByQuery[query] == nil, @"We already listen to query: %@", query);
+
+ FSTQueryData *queryData = [self.localStore allocateQuery:query];
+ FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
+ FSTDocumentKeySet *remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:remoteKeys];
+ FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs];
+ FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges];
+ FSTAssert(viewChange.limboChanges.count == 0,
+ @"View returned limbo docs before target ack from the server.");
+
+ FSTQueryView *queryView = [[FSTQueryView alloc] initWithQuery:query
+ targetID:queryData.targetID
+ resumeToken:queryData.resumeToken
+ view:view];
+ self.queryViewsByQuery[query] = queryView;
+ self.queryViewsByTarget[@(queryData.targetID)] = queryView;
+ [self.delegate handleViewSnapshots:@[ viewChange.snapshot ]];
+
+ [self.remoteStore listenToTargetWithQueryData:queryData];
+ return queryData.targetID;
+}
+
+- (void)stopListeningToQuery:(FSTQuery *)query {
+ [self assertDelegateExistsForSelector:_cmd];
+
+ FSTQueryView *queryView = self.queryViewsByQuery[query];
+ FSTAssert(queryView, @"Trying to stop listening to a query not found");
+
+ [self.localStore releaseQuery:query];
+ [self.remoteStore stopListeningToTargetID:queryView.targetID];
+ [self removeAndCleanupQuery:queryView];
+ [self.localStore collectGarbage];
+}
+
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations
+ completion:(FSTVoidErrorBlock)completion {
+ [self assertDelegateExistsForSelector:_cmd];
+
+ FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations];
+ [self addMutationCompletionBlock:completion batchID:result.batchID];
+
+ [self emitNewSnapshotsWithChanges:result.changes remoteEvent:nil];
+ [self.remoteStore fillWritePipeline];
+}
+
+- (void)addMutationCompletionBlock:(FSTVoidErrorBlock)completion batchID:(FSTBatchID)batchID {
+ NSMutableDictionary<NSNumber *, FSTVoidErrorBlock> *completionBlocks =
+ self.mutationCompletionBlocks[self.currentUser];
+ if (!completionBlocks) {
+ completionBlocks = [NSMutableDictionary dictionary];
+ self.mutationCompletionBlocks[self.currentUser] = completionBlocks;
+ }
+ [completionBlocks setObject:completion forKey:@(batchID)];
+}
+
+/**
+ * Takes an updateBlock in which a set of reads and writes can be performed atomically. In the
+ * updateBlock, user code can read and write values using a transaction object. After the
+ * updateBlock, all changes will be committed. If someone else has changed any of the data
+ * referenced, then the updateBlock will be called again. If the updateBlock still fails after the
+ * given number of retries, then the transaction will be rejected.
+ *
+ * The transaction object passed to the updateBlock contains methods for accessing documents
+ * and collections. Unlike other firestore access, data accessed with the transaction will not
+ * reflect local changes that have not been committed. For this reason, it is required that all
+ * reads are performed before any writes. Transactions must be performed while online.
+ */
+- (void)transactionWithRetries:(int)retries
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ updateBlock:(FSTTransactionBlock)updateBlock
+ completion:(FSTVoidIDErrorBlock)completion {
+ [workerDispatchQueue verifyIsCurrentQueue];
+ FSTAssert(retries >= 0, @"Got negative number of retries for transaction");
+ FSTTransaction *transaction = [self.remoteStore transaction];
+ updateBlock(transaction, ^(id _Nullable result, NSError *_Nullable error) {
+ [workerDispatchQueue dispatchAsync:^{
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+ [transaction commitWithCompletion:^(NSError *_Nullable transactionError) {
+ if (!transactionError) {
+ completion(result, nil);
+ return;
+ }
+ // TODO(b/35201829): Only retry on real transaction failures.
+ if (retries == 0) {
+ NSError *wrappedError =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeFailedPrecondition
+ userInfo:@{
+ NSLocalizedDescriptionKey : @"Transaction failed all retries.",
+ NSUnderlyingErrorKey : transactionError
+ }];
+ completion(nil, wrappedError);
+ return;
+ }
+ [workerDispatchQueue verifyIsCurrentQueue];
+ return [self transactionWithRetries:(retries - 1)
+ workerDispatchQueue:workerDispatchQueue
+ updateBlock:updateBlock
+ completion:completion];
+ }];
+ }];
+ });
+}
+
+- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
+ [self assertDelegateExistsForSelector:_cmd];
+
+ // Make sure limbo documents are deleted if there were no results
+ [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^(
+ FSTBoxedTargetID *_Nonnull targetID,
+ FSTTargetChange *_Nonnull targetChange, BOOL *_Nonnull stop) {
+ FSTDocumentKey *limboKey = self.limboKeysByTarget[targetID];
+ if (limboKey && targetChange.currentStatusUpdate == FSTCurrentStatusUpdateMarkCurrent &&
+ remoteEvent.documentUpdates[limboKey] == nil) {
+ // When listening to a query the server responds with a snapshot containing documents
+ // matching the query and a current marker telling us we're now in sync. It's possible for
+ // these to arrive as separate remote events or as a single remote event. For a document
+ // query, there will be no documents sent in the response if the document doesn't exist.
+ //
+ // If the snapshot arrives separately from the current marker, we handle it normally and
+ // updateTrackedLimboDocumentsWithChanges:targetID: will resolve the limbo status of the
+ // document, removing it from limboDocumentRefs. This works because clients only initiate
+ // limbo resolution when a target is current and because all current targets are always at a
+ // consistent snapshot.
+ //
+ // However, if the document doesn't exist and the current marker arrives, the document is
+ // not present in the snapshot and our normal view handling would consider the document to
+ // remain in limbo indefinitely because there are no updates to the document. To avoid this,
+ // we specially handle this just this case here: synthesizing a delete.
+ //
+ // TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an
+ // explicit delete message and we could remove this special logic.
+ [remoteEvent
+ addDocumentUpdate:[FSTDeletedDocument documentWithKey:limboKey
+ version:remoteEvent.snapshotVersion]];
+ }
+ }];
+
+ FSTMaybeDocumentDictionary *changes = [self.localStore applyRemoteEvent:remoteEvent];
+ [self emitNewSnapshotsWithChanges:changes remoteEvent:remoteEvent];
+}
+
+- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error {
+ [self assertDelegateExistsForSelector:_cmd];
+
+ FSTDocumentKey *limboKey = self.limboKeysByTarget[targetID];
+ if (limboKey) {
+ // Since this query failed, we won't want to manually unlisten to it.
+ // So go ahead and remove it from bookkeeping.
+ [self.limboTargetsByKey removeObjectForKey:limboKey];
+ [self.limboKeysByTarget removeObjectForKey:targetID];
+
+ // TODO(dimond): Retry on transient errors?
+
+ // It's a limbo doc. Create a synthetic event saying it was deleted. This is kind of a hack.
+ // Ideally, we would have a method in the local store to purge a document. However, it would
+ // be tricky to keep all of the local store's invariants with another method.
+ NSMutableDictionary<NSNumber *, FSTTargetChange *> *targetChanges =
+ [NSMutableDictionary dictionary];
+ FSTDeletedDocument *doc =
+ [FSTDeletedDocument documentWithKey:limboKey version:[FSTSnapshotVersion noVersion]];
+ NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *docUpdate =
+ [NSMutableDictionary dictionaryWithObject:doc forKey:limboKey];
+ FSTRemoteEvent *event = [FSTRemoteEvent eventWithSnapshotVersion:[FSTSnapshotVersion noVersion]
+ targetChanges:targetChanges
+ documentUpdates:docUpdate];
+ [self applyRemoteEvent:event];
+ } else {
+ FSTQueryView *queryView = self.queryViewsByTarget[targetID];
+ FSTAssert(queryView, @"Unknown targetId: %@", targetID);
+ [self.localStore releaseQuery:queryView.query];
+ [self removeAndCleanupQuery:queryView];
+ [self.delegate handleError:error forQuery:queryView.query];
+ }
+}
+
+- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult {
+ [self assertDelegateExistsForSelector:_cmd];
+
+ // The local store may or may not be able to apply the write result and raise events immediately
+ // (depending on whether the watcher is caught up), so we raise user callbacks first so that they
+ // consistently happen before listen events.
+ [self processUserCallbacksForBatchID:batchResult.batch.batchID error:nil];
+
+ FSTMaybeDocumentDictionary *changes = [self.localStore acknowledgeBatchWithResult:batchResult];
+ [self emitNewSnapshotsWithChanges:changes remoteEvent:nil];
+}
+
+- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error {
+ [self assertDelegateExistsForSelector:_cmd];
+
+ // The local store may or may not be able to apply the write result and raise events immediately
+ // (depending on whether the watcher is caught up), so we raise user callbacks first so that they
+ // consistently happen before listen events.
+ [self processUserCallbacksForBatchID:batchID error:error];
+
+ FSTMaybeDocumentDictionary *changes = [self.localStore rejectBatchID:batchID];
+ [self emitNewSnapshotsWithChanges:changes remoteEvent:nil];
+}
+
+- (void)processUserCallbacksForBatchID:(FSTBatchID)batchID error:(NSError *_Nullable)error {
+ NSMutableDictionary<NSNumber *, FSTVoidErrorBlock> *completionBlocks =
+ self.mutationCompletionBlocks[self.currentUser];
+
+ // NOTE: Mutations restored from persistence won't have completion blocks, so it's okay for
+ // this (or the completion below) to be nil.
+ if (completionBlocks) {
+ NSNumber *boxedBatchID = @(batchID);
+ FSTVoidErrorBlock completion = completionBlocks[boxedBatchID];
+ if (completion) {
+ completion(error);
+ [completionBlocks removeObjectForKey:boxedBatchID];
+ }
+ }
+}
+
+- (void)assertDelegateExistsForSelector:(SEL)methodSelector {
+ FSTAssert(self.delegate, @"Tried to call '%@' before delegate was registered.",
+ NSStringFromSelector(methodSelector));
+}
+
+- (void)removeAndCleanupQuery:(FSTQueryView *)queryView {
+ [self.queryViewsByQuery removeObjectForKey:queryView.query];
+ [self.queryViewsByTarget removeObjectForKey:@(queryView.targetID)];
+
+ [self.limboDocumentRefs removeReferencesForID:queryView.targetID];
+ [self garbageCollectLimboDocuments];
+}
+
+/**
+ * Computes a new snapshot from the changes and calls the registered callback with the new snapshot.
+ */
+- (void)emitNewSnapshotsWithChanges:(FSTMaybeDocumentDictionary *)changes
+ remoteEvent:(FSTRemoteEvent *_Nullable)remoteEvent {
+ NSMutableArray<FSTViewSnapshot *> *newSnapshots = [NSMutableArray array];
+ NSMutableArray<FSTLocalViewChanges *> *documentChangesInAllViews = [NSMutableArray array];
+
+ [self.queryViewsByQuery
+ enumerateKeysAndObjectsUsingBlock:^(FSTQuery *query, FSTQueryView *queryView, BOOL *stop) {
+ FSTView *view = queryView.view;
+ FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:changes];
+ if (viewDocChanges.needsRefill) {
+ // The query has a limit and some docs were removed/updated, so we need to re-run the
+ // query against the local store to make sure we didn't lose any good docs that had been
+ // past the limit.
+ FSTDocumentDictionary *docs = [self.localStore executeQuery:queryView.query];
+ viewDocChanges = [view computeChangesWithDocuments:docs previousChanges:viewDocChanges];
+ }
+ FSTTargetChange *_Nullable targetChange = remoteEvent.targetChanges[@(queryView.targetID)];
+ FSTViewChange *viewChange =
+ [queryView.view applyChangesToDocuments:viewDocChanges targetChange:targetChange];
+
+ [self updateTrackedLimboDocumentsWithChanges:viewChange.limboChanges
+ targetID:queryView.targetID];
+
+ if (viewChange.snapshot) {
+ [newSnapshots addObject:viewChange.snapshot];
+ FSTLocalViewChanges *docChanges =
+ [FSTLocalViewChanges changesForViewSnapshot:viewChange.snapshot];
+ [documentChangesInAllViews addObject:docChanges];
+ }
+ }];
+
+ [self.delegate handleViewSnapshots:newSnapshots];
+ [self.localStore notifyLocalViewChanges:documentChangesInAllViews];
+ [self.localStore collectGarbage];
+}
+
+/** Updates the limbo document state for the given targetID. */
+- (void)updateTrackedLimboDocumentsWithChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges
+ targetID:(FSTTargetID)targetID {
+ for (FSTLimboDocumentChange *limboChange in limboChanges) {
+ switch (limboChange.type) {
+ case FSTLimboDocumentChangeTypeAdded:
+ [self.limboDocumentRefs addReferenceToKey:limboChange.key forID:targetID];
+ [self trackLimboChange:limboChange];
+ break;
+
+ case FSTLimboDocumentChangeTypeRemoved:
+ FSTLog(@"Document no longer in limbo: %@", limboChange.key);
+ [self.limboDocumentRefs removeReferenceToKey:limboChange.key forID:targetID];
+ break;
+
+ default:
+ FSTFail(@"Unknown limbo change type: %ld", (long)limboChange.type);
+ }
+ }
+ [self garbageCollectLimboDocuments];
+}
+
+- (void)trackLimboChange:(FSTLimboDocumentChange *)limboChange {
+ FSTDocumentKey *key = limboChange.key;
+
+ if (!self.limboTargetsByKey[key]) {
+ FSTLog(@"New document in limbo: %@", key);
+ FSTTargetID limboTargetID = [self.targetIdGenerator nextID];
+ FSTQuery *query = [FSTQuery queryWithPath:key.path];
+ FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query
+ targetID:limboTargetID
+ purpose:FSTQueryPurposeLimboResolution];
+ self.limboKeysByTarget[@(limboTargetID)] = key;
+ [self.remoteStore listenToTargetWithQueryData:queryData];
+ self.limboTargetsByKey[key] = @(limboTargetID);
+ }
+}
+
+/** Garbage collect the limbo documents that we no longer need to track. */
+- (void)garbageCollectLimboDocuments {
+ NSSet<FSTDocumentKey *> *garbage = [self.limboCollector collectGarbage];
+ for (FSTDocumentKey *key in garbage) {
+ FSTBoxedTargetID *limboTarget = self.limboTargetsByKey[key];
+ if (!limboTarget) {
+ // This target already got removed, because the query failed.
+ return;
+ }
+ FSTTargetID limboTargetID = limboTarget.intValue;
+ [self.remoteStore stopListeningToTargetID:limboTargetID];
+ [self.limboTargetsByKey removeObjectForKey:key];
+ [self.limboKeysByTarget removeObjectForKey:limboTarget];
+ }
+}
+
+// Used for testing
+- (NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments {
+ // Return defensive copy
+ return [self.limboTargetsByKey copy];
+}
+
+- (void)userDidChange:(FSTUser *)user {
+ self.currentUser = user;
+
+ // Notify local store and emit any resulting events from swapping out the mutation queue.
+ FSTMaybeDocumentDictionary *changes = [self.localStore userDidChange:user];
+ [self emitNewSnapshotsWithChanges:changes remoteEvent:nil];
+
+ // Notify remote store so it can restart its streams.
+ [self.remoteStore userDidChange:user];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTTargetIDGenerator.h b/Firestore/Source/Core/FSTTargetIDGenerator.h
new file mode 100644
index 0000000..5b9db10
--- /dev/null
+++ b/Firestore/Source/Core/FSTTargetIDGenerator.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 "FSTTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTTargetIDGenerator generates monotonically increasing integer IDs. There are separate
+ * generators for different scopes. While these generators will operate independently of each
+ * other, they are scoped, such that no two generators will ever produce the same ID. This is
+ * useful, because sometimes the backend may group IDs from separate parts of the client into the
+ * same ID space.
+ */
+@interface FSTTargetIDGenerator : NSObject
+
+/**
+ * Creates and returns the FSTTargetIDGenerator for the local store.
+ *
+ * @param after An ID to start at. Every call to nextID will return an ID > @a after.
+ * @return A shared instance of FSTTargetIDGenerator.
+ */
++ (instancetype)generatorForLocalStoreStartingAfterID:(FSTTargetID)after;
+
+/**
+ * Creates and returns the FSTTargetIDGenerator for the sync engine.
+ *
+ * @param after An ID to start at. Every call to nextID will return an ID > @a after.
+ * @return A shared instance of FSTTargetIDGenerator.
+ */
++ (instancetype)generatorForSyncEngineStartingAfterID:(FSTTargetID)after;
+
+- (id)init __attribute__((unavailable("Use a static constructor method.")));
+
+/** Returns the next ID in the sequence. */
+- (FSTTargetID)nextID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTTargetIDGenerator.m b/Firestore/Source/Core/FSTTargetIDGenerator.m
new file mode 100644
index 0000000..86ded30
--- /dev/null
+++ b/Firestore/Source/Core/FSTTargetIDGenerator.m
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTTargetIDGenerator.h"
+
+#import <libkern/OSAtomic.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTTargetIDGenerator
+
+static const int kReservedBits = 1;
+
+/** FSTTargetIDGeneratorID is the set of all valid generators. */
+typedef NS_ENUM(NSInteger, FSTTargetIDGeneratorID) {
+ FSTTargetIDGeneratorIDLocalStore = 0,
+ FSTTargetIDGeneratorIDSyncEngine = 1
+};
+
+@interface FSTTargetIDGenerator () {
+ // This is volatile so it can be used with OSAtomicAdd32.
+ volatile FSTTargetID _previousID;
+}
+
+/**
+ * Initializes the generator.
+ *
+ * @param generatorID A unique ID indicating which generator this is.
+ * @param after Every call to nextID will return a number > @a after.
+ */
+- (instancetype)initWithGeneratorID:(FSTTargetIDGeneratorID)generatorID
+ startingAfterID:(FSTTargetID)after NS_DESIGNATED_INITIALIZER;
+
+// This is typed as FSTTargetID because we need to do bitwise operations with them together.
+@property(nonatomic, assign) FSTTargetID generatorID;
+@end
+
+@implementation FSTTargetIDGenerator
+
+#pragma mark - Constructors
+
+- (instancetype)initWithGeneratorID:(FSTTargetIDGeneratorID)generatorID
+ startingAfterID:(FSTTargetID)after {
+ self = [super init];
+ if (self) {
+ _generatorID = generatorID;
+
+ // Replace the generator part of |after| with this generator's ID.
+ FSTTargetID afterWithoutGenerator = (after >> kReservedBits) << kReservedBits;
+ FSTTargetID afterGenerator = after - afterWithoutGenerator;
+ if (afterGenerator >= _generatorID) {
+ // For example, if:
+ // self.generatorID = 0b0000
+ // after = 0b1011
+ // afterGenerator = 0b0001
+ // Then:
+ // previous = 0b1010
+ // next = 0b1100
+ _previousID = afterWithoutGenerator | self.generatorID;
+ } else {
+ // For example, if:
+ // self.generatorID = 0b0001
+ // after = 0b1010
+ // afterGenerator = 0b0000
+ // Then:
+ // previous = 0b1001
+ // next = 0b1011
+ _previousID = (afterWithoutGenerator | self.generatorID) - (1 << kReservedBits);
+ }
+ }
+ return self;
+}
+
++ (instancetype)generatorForLocalStoreStartingAfterID:(FSTTargetID)after {
+ return [[FSTTargetIDGenerator alloc] initWithGeneratorID:FSTTargetIDGeneratorIDLocalStore
+ startingAfterID:after];
+}
+
++ (instancetype)generatorForSyncEngineStartingAfterID:(FSTTargetID)after {
+ return [[FSTTargetIDGenerator alloc] initWithGeneratorID:FSTTargetIDGeneratorIDSyncEngine
+ startingAfterID:after];
+}
+
+#pragma mark - Public methods
+
+- (FSTTargetID)nextID {
+ return OSAtomicAdd32(1 << kReservedBits, &_previousID);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTTimestamp.h b/Firestore/Source/Core/FSTTimestamp.h
new file mode 100644
index 0000000..f86779d
--- /dev/null
+++ b/Firestore/Source/Core/FSTTimestamp.h
@@ -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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An FSTTimestamp represents an absolute time from the backend at up to nanosecond precision.
+ * An FSTTimestamp is represented in terms of UTC and does not have an associated timezone.
+ */
+@interface FSTTimestamp : NSObject <NSCopying>
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Creates a new timestamp.
+ *
+ * @param seconds the number of seconds since epoch.
+ * @param nanos the number of nanoseconds after the seconds.
+ */
+- (instancetype)initWithSeconds:(int64_t)seconds nanos:(int32_t)nanos NS_DESIGNATED_INITIALIZER;
+
+/** Creates a new timestamp with the current date / time. */
++ (instancetype)timestamp;
+
+/** Creates a new timestamp from the given date. */
++ (instancetype)timestampWithDate:(NSDate *)date;
+
+/** Returns a new NSDate corresponding to this timestamp. This may lose precision. */
+- (NSDate *)approximateDateValue;
+
+/**
+ * Converts the given date to a an ISO 8601 timestamp string, useful for rendering in JSON.
+ *
+ * ISO 8601 dates times in UTC look like this: "1912-04-14T23:40:00.000000000Z".
+ *
+ * @see http://www.ecma-international.org/ecma-262/6.0/#sec-date-time-string-format
+ */
+- (NSString *)ISO8601String;
+
+- (NSComparisonResult)compare:(FSTTimestamp *)other;
+
+/**
+ * Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z.
+ * Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.
+ */
+@property(nonatomic, assign, readonly) int64_t seconds;
+
+/**
+ * Non-negative fractions of a second at nanosecond resolution. Negative second values with
+ * fractions must still have non-negative nanos values that count forward in time.
+ * Must be from 0 to 999,999,999 inclusive.
+ */
+@property(nonatomic, assign, readonly) int32_t nanos;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTTimestamp.m b/Firestore/Source/Core/FSTTimestamp.m
new file mode 100644
index 0000000..941217a
--- /dev/null
+++ b/Firestore/Source/Core/FSTTimestamp.m
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTTimestamp.h"
+
+#import "FSTAssert.h"
+#import "FSTComparison.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static const int kNanosPerSecond = 1000000000;
+
+@implementation FSTTimestamp
+
+#pragma mark - Constructors
+
++ (instancetype)timestamp {
+ return [FSTTimestamp timestampWithDate:[NSDate date]];
+}
+
++ (instancetype)timestampWithDate:(NSDate *)date {
+ double secondsDouble;
+ double fraction = modf(date.timeIntervalSince1970, &secondsDouble);
+ // GCP Timestamps always have non-negative nanos.
+ if (fraction < 0) {
+ fraction += 1.0;
+ secondsDouble -= 1.0;
+ }
+ int64_t seconds = (int64_t)secondsDouble;
+ int32_t nanos = (int32_t)(fraction * kNanosPerSecond);
+ return [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos];
+}
+
+- (instancetype)initWithSeconds:(int64_t)seconds nanos:(int32_t)nanos {
+ self = [super init];
+ if (self) {
+ FSTAssert(nanos >= 0, @"timestamp nanoseconds out of range: %d", nanos);
+ FSTAssert(nanos < 1e9, @"timestamp nanoseconds out of range: %d", nanos);
+ // Midnight at the beginning of 1/1/1 is the earliest timestamp Firestore supports.
+ FSTAssert(seconds >= -62135596800L, @"timestamp seconds out of range: %lld", seconds);
+ // This will break in the year 10,000.
+ FSTAssert(seconds < 253402300800L, @"timestamp seconds out of range: %lld", seconds);
+
+ _seconds = seconds;
+ _nanos = nanos;
+ }
+ return self;
+}
+
+#pragma mark - NSObject methods
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+ if (![object isKindOfClass:[FSTTimestamp class]]) {
+ return NO;
+ }
+ return [self isEqualToTimestamp:(FSTTimestamp *)object];
+}
+
+- (NSUInteger)hash {
+ return (NSUInteger)((self.seconds >> 32) ^ self.seconds ^ self.nanos);
+}
+
+- (NSString *)description {
+ return [NSString
+ stringWithFormat:@"<FSTTimestamp: seconds=%lld nanos=%d>", self.seconds, self.nanos];
+}
+
+/** Implements NSCopying without actually copying because timestamps are immutable. */
+- (id)copyWithZone:(NSZone *_Nullable)zone {
+ return self;
+}
+
+#pragma mark - Public methods
+
+- (NSDate *)approximateDateValue {
+ NSTimeInterval interval = (NSTimeInterval)self.seconds + ((NSTimeInterval)self.nanos) / 1e9;
+ return [NSDate dateWithTimeIntervalSince1970:interval];
+}
+
+- (BOOL)isEqualToTimestamp:(FSTTimestamp *)other {
+ return [self compare:other] == NSOrderedSame;
+}
+
+- (NSString *)ISO8601String {
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+ formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss";
+ formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
+ NSDate *secondsDate = [NSDate dateWithTimeIntervalSince1970:self.seconds];
+ NSString *secondsString = [formatter stringFromDate:secondsDate];
+ FSTAssert(secondsString.length == 19, @"Invalid ISO string: %@", secondsString);
+
+ NSString *nanosString = [NSString stringWithFormat:@"%09d", self.nanos];
+ return [NSString stringWithFormat:@"%@.%@Z", secondsString, nanosString];
+}
+
+- (NSComparisonResult)compare:(FSTTimestamp *)other {
+ NSComparisonResult result = FSTCompareInt64s(self.seconds, other.seconds);
+ if (result != NSOrderedSame) {
+ return result;
+ }
+ return FSTCompareInt32s(self.nanos, other.nanos);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTTransaction.h b/Firestore/Source/Core/FSTTransaction.h
new file mode 100644
index 0000000..7fa3a10
--- /dev/null
+++ b/Firestore/Source/Core/FSTTransaction.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FSTTypes.h"
+
+@class FIRSetOptions;
+@class FSTDatastore;
+@class FSTDocumentKey;
+@class FSTFieldMask;
+@class FSTFieldTransform;
+@class FSTMaybeDocument;
+@class FSTObjectValue;
+@class FSTParsedSetData;
+@class FSTParsedUpdateData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTTransaction
+
+/** Provides APIs to use in a transaction context. */
+@interface FSTTransaction : NSObject
+
+/** Creates a new transaction object, which can only be used for one transaction attempt. **/
++ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore;
+
+/**
+ * Takes a set of keys and asynchronously attempts to fetch all the documents from the backend,
+ * ignoring any local changes.
+ */
+- (void)lookupDocumentsForKeys:(NSArray<FSTDocumentKey *> *)keys
+ completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion;
+
+/**
+ * Stores mutation for the given key and set data, to be committed when commitWithCompletion is
+ * called.
+ */
+- (void)setData:(FSTParsedSetData *)data forDocument:(FSTDocumentKey *)key;
+
+/**
+ * Stores mutations for the given key and update data, to be committed when commitWithCompletion
+ * is called.
+ */
+- (void)updateData:(FSTParsedUpdateData *)data forDocument:(FSTDocumentKey *)key;
+
+/**
+ * Stores a delete mutation for the given key, to be committed when commitWithCompletion is called.
+ */
+- (void)deleteDocument:(FSTDocumentKey *)key;
+
+/**
+ * Attempts to commit the mutations set on this transaction. Calls the given completion block when
+ * finished. Once this is called, no other mutations or commits are allowed on the transaction.
+ */
+- (void)commitWithCompletion:(FSTVoidErrorBlock)completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTTransaction.m b/Firestore/Source/Core/FSTTransaction.m
new file mode 100644
index 0000000..26c69e0
--- /dev/null
+++ b/Firestore/Source/Core/FSTTransaction.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 "FSTTransaction.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+#import "FIRFirestoreErrors.h"
+#import "FIRSetOptions.h"
+#import "FSTAssert.h"
+#import "FSTDatastore.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTDocumentKeySet.h"
+#import "FSTMutation.h"
+#import "FSTSnapshotVersion.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTTransaction
+
+@interface FSTTransaction ()
+@property(nonatomic, strong, readonly) FSTDatastore *datastore;
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTDocumentKey *, FSTSnapshotVersion *> *readVersions;
+@property(nonatomic, strong, readonly) NSMutableArray *mutations;
+@property(nonatomic, assign) BOOL commitCalled;
+/**
+ * An error that may have occurred as a consequence of a write. If set, needs to be raised in the
+ * completion handler instead of trying to commit.
+ */
+@property(nonatomic, strong, nullable) NSError *lastWriteError;
+@end
+
+@implementation FSTTransaction
+
++ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore {
+ return [[FSTTransaction alloc] initWithDatastore:datastore];
+}
+
+- (instancetype)initWithDatastore:(FSTDatastore *)datastore {
+ self = [super init];
+ if (self) {
+ _datastore = datastore;
+ _readVersions = [NSMutableDictionary dictionary];
+ _mutations = [NSMutableArray array];
+ _commitCalled = NO;
+ }
+ return self;
+}
+
+/**
+ * Every time a document is read, this should be called to record its version. If we read two
+ * different versions of the same document, this will return an error through its out parameter.
+ * When the transaction is committed, the versions recorded will be set as preconditions on the
+ * writes sent to the backend.
+ */
+- (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error {
+ FSTAssert(error != nil, @"nil error parameter");
+ *error = nil;
+ FSTSnapshotVersion *docVersion = doc.version;
+ if ([doc isKindOfClass:[FSTDeletedDocument class]]) {
+ // For deleted docs, we must record an explicit no version to build the right precondition
+ // when writing.
+ docVersion = [FSTSnapshotVersion noVersion];
+ }
+ FSTSnapshotVersion *existingVersion = self.readVersions[doc.key];
+ if (existingVersion) {
+ if (error) {
+ *error =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeFailedPrecondition
+ userInfo:@{
+ NSLocalizedDescriptionKey :
+ @"A document cannot be read twice within a single transaction."
+ }];
+ }
+ return NO;
+ } else {
+ self.readVersions[doc.key] = docVersion;
+ return YES;
+ }
+}
+
+- (void)lookupDocumentsForKeys:(NSArray<FSTDocumentKey *> *)keys
+ completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion {
+ [self ensureCommitNotCalled];
+ if (self.mutations.count) {
+ FSTThrowInvalidUsage(@"FIRIllegalStateException",
+ @"All reads in a transaction must be done before any writes.");
+ }
+ [self.datastore
+ lookupDocuments:keys
+ completion:^(NSArray<FSTDocument *> *_Nullable documents, NSError *_Nullable error) {
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+ for (FSTMaybeDocument *doc in documents) {
+ NSError *recordError = nil;
+ if (![self recordVersionForDocument:doc error:&recordError]) {
+ completion(nil, recordError);
+ return;
+ }
+ }
+ completion(documents, nil);
+ }];
+}
+
+/** Stores mutations to be written when commitWithCompletion is called. */
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
+ [self ensureCommitNotCalled];
+ [self.mutations addObjectsFromArray:mutations];
+}
+
+/**
+ * Returns version of this doc when it was read in this transaction as a precondition, or no
+ * precondition if it was not read.
+ */
+- (FSTPrecondition *)preconditionForDocumentKey:(FSTDocumentKey *)key {
+ FSTSnapshotVersion *_Nullable snapshotVersion = self.readVersions[key];
+ if (snapshotVersion) {
+ return [FSTPrecondition preconditionWithUpdateTime:snapshotVersion];
+ } else {
+ return [FSTPrecondition none];
+ }
+}
+
+/**
+ * Returns the precondition for a document if the operation is an update, based on the provided
+ * UpdateOptions. Will return nil if an error occurred, in which case it sets the error parameter.
+ */
+- (nullable FSTPrecondition *)preconditionForUpdateWithDocumentKey:(FSTDocumentKey *)key
+ error:(NSError **)error {
+ FSTSnapshotVersion *_Nullable version = self.readVersions[key];
+ if (version && [version isEqual:[FSTSnapshotVersion noVersion]]) {
+ // The document was read, but doesn't exist.
+ // Return an error because the precondition is impossible
+ if (error) {
+ *error = [NSError
+ errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeAborted
+ userInfo:@{
+ NSLocalizedDescriptionKey : @"Can't update a document that doesn't exist."
+ }];
+ }
+ return nil;
+ } else if (version) {
+ // Document exists, just base precondition on document update time.
+ return [FSTPrecondition preconditionWithUpdateTime:version];
+ } else {
+ // Document was not read, so we just use the preconditions for an update.
+ return [FSTPrecondition preconditionWithExists:YES];
+ }
+}
+
+- (void)setData:(FSTParsedSetData *)data forDocument:(FSTDocumentKey *)key {
+ [self writeMutations:[data mutationsWithKey:key
+ precondition:[self preconditionForDocumentKey:key]]];
+}
+
+- (void)updateData:(FSTParsedUpdateData *)data forDocument:(FSTDocumentKey *)key {
+ NSError *error = nil;
+ FSTPrecondition *_Nullable precondition =
+ [self preconditionForUpdateWithDocumentKey:key error:&error];
+ if (precondition) {
+ [self writeMutations:[data mutationsWithKey:key precondition:precondition]];
+ } else {
+ FSTAssert(error, @"Got nil precondition, but error was not set");
+ self.lastWriteError = error;
+ }
+}
+
+- (void)deleteDocument:(FSTDocumentKey *)key {
+ [self writeMutations:@[ [[FSTDeleteMutation alloc]
+ initWithKey:key
+ precondition:[self preconditionForDocumentKey:key]] ]];
+ // Since the delete will be applied before all following writes, we need to ensure that the
+ // precondition for the next write will be exists: false.
+ self.readVersions[key] = [FSTSnapshotVersion noVersion];
+}
+
+- (void)commitWithCompletion:(FSTVoidErrorBlock)completion {
+ [self ensureCommitNotCalled];
+ // Once commitWithCompletion is called once, mark this object so it can't be used again.
+ self.commitCalled = YES;
+
+ // If there was an error writing, raise that error now
+ if (self.lastWriteError) {
+ completion(self.lastWriteError);
+ return;
+ }
+
+ // Make a list of read documents that haven't been written.
+ __block FSTDocumentKeySet *unwritten = [FSTDocumentKeySet keySet];
+ [self.readVersions enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key,
+ FSTSnapshotVersion *version, BOOL *stop) {
+ unwritten = [unwritten setByAddingObject:key];
+ }];
+ // For each mutation, note that the doc was written.
+ for (FSTMutation *mutation in self.mutations) {
+ unwritten = [unwritten setByRemovingObject:mutation.key];
+ }
+ if (unwritten.count) {
+ // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend.
+ completion([NSError
+ errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeFailedPrecondition
+ userInfo:@{
+ NSLocalizedDescriptionKey : @"Every document read in a transaction must also be "
+ @"written in that transaction."
+ }]);
+ } else {
+ [self.datastore commitMutations:self.mutations
+ completion:^(NSError *_Nullable error) {
+ if (error) {
+ completion(error);
+ } else {
+ completion(nil);
+ }
+ }];
+ }
+}
+
+- (void)ensureCommitNotCalled {
+ if (self.commitCalled) {
+ FSTThrowInvalidUsage(
+ @"FIRIllegalStateException",
+ @"A transaction object cannot be used after its update block has completed.");
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTTypes.h b/Firestore/Source/Core/FSTTypes.h
new file mode 100644
index 0000000..8f1183c
--- /dev/null
+++ b/Firestore/Source/Core/FSTTypes.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FSTMaybeDocument;
+@class FSTTransaction;
+
+/** FSTBatchID is a locally assigned ID for a batch of mutations that have been applied. */
+typedef int32_t FSTBatchID;
+
+typedef int32_t FSTTargetID;
+
+typedef NSNumber FSTBoxedTargetID;
+
+/**
+ * FSTVoidBlock is a block that's called when a specific event happens but that otherwise has
+ * no information associated with it.
+ */
+typedef void (^FSTVoidBlock)();
+
+/**
+ * FSTVoidErrorBlock is a block that gets an error, if one occurred.
+ *
+ * @param error The error if it occurred, or nil.
+ */
+typedef void (^FSTVoidErrorBlock)(NSError *_Nullable error);
+
+/** FSTVoidIDErrorBlock is a block that takes an optional value and error. */
+typedef void (^FSTVoidIDErrorBlock)(id _Nullable, NSError *_Nullable);
+
+/**
+ * FSTVoidMaybeDocumentErrorBlock is a block that gets either a list of documents or an error.
+ *
+ * @param documents The documents, if no error occurred, or nil.
+ * @param error The error, if one occurred, or nil.
+ */
+typedef void (^FSTVoidMaybeDocumentArrayErrorBlock)(
+ NSArray<FSTMaybeDocument *> *_Nullable documents, NSError *_Nullable error);
+
+/**
+ * FSTTransactionBlock is a block that wraps a user's transaction update block internally.
+ *
+ * @param transaction An object with methods for performing reads and writes within the
+ * transaction.
+ * @param completion To be called by the block once the user's code is finished.
+ */
+typedef void (^FSTTransactionBlock)(FSTTransaction *transaction,
+ void (^completion)(id _Nullable, NSError *_Nullable));
+
+/** Describes the online state of the Firestore client */
+typedef NS_ENUM(NSUInteger, FSTOnlineState) {
+ /**
+ * The Firestore client is in an unknown online state. This means the client is either not
+ * actively trying to establish a connection or it was previously in an unknown state and is
+ * trying to establish a connection.
+ */
+ FSTOnlineStateUnknown,
+
+ /**
+ * The client is connected and the connections are healthy. This state is reached after a
+ * successful connection and there has been at least one successful message received from the
+ * backends.
+ */
+ FSTOnlineStateHealthy,
+
+ /**
+ * The client has tried to establish a connection but has failed.
+ * This state is reached after either a connection attempt failed or a healthy stream was closed
+ * for unexpected reasons.
+ */
+ FSTOnlineStateFailed
+};
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h
new file mode 100644
index 0000000..2dbfac2
--- /dev/null
+++ b/Firestore/Source/Core/FSTView.h
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTDocumentDictionary.h"
+#import "FSTDocumentKeySet.h"
+
+@class FSTDocumentKey;
+@class FSTDocumentSet;
+@class FSTDocumentViewChangeSet;
+@class FSTMaybeDocument;
+@class FSTQuery;
+@class FSTTargetChange;
+@class FSTViewSnapshot;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTViewDocumentChanges
+
+/** The result of applying a set of doc changes to a view. */
+@interface FSTViewDocumentChanges : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** The new set of docs that should be in the view. */
+@property(nonatomic, strong, readonly) FSTDocumentSet *documentSet;
+
+/** The diff of this these docs with the previous set of docs. */
+@property(nonatomic, strong, readonly) FSTDocumentViewChangeSet *changeSet;
+
+/**
+ * Whether the set of documents passed in was not sufficient to calculate the new state of the view
+ * and there needs to be another pass based on the local cache.
+ */
+@property(nonatomic, assign, readonly) BOOL needsRefill;
+
+@property(nonatomic, strong, readonly) FSTDocumentKeySet *mutatedKeys;
+
+@end
+
+#pragma mark - FSTLimboDocumentChange
+
+typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) {
+ FSTLimboDocumentChangeTypeAdded = 0,
+ FSTLimboDocumentChangeTypeRemoved,
+};
+
+// A change to a particular document wrt to whether it is in "limbo".
+@interface FSTLimboDocumentChange : NSObject
+
++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key;
+
+- (id)init __attribute__((unavailable("Use a static constructor method.")));
+
+@property(nonatomic, assign, readonly) FSTLimboDocumentChangeType type;
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+@end
+
+#pragma mark - FSTViewChange
+
+// A set of changes to a view.
+@interface FSTViewChange : NSObject
+
+- (id)init __attribute__((unavailable("Use a static constructor method.")));
+
+@property(nonatomic, strong, readonly, nullable) FSTViewSnapshot *snapshot;
+@property(nonatomic, strong, readonly) NSArray<FSTLimboDocumentChange *> *limboChanges;
+@end
+
+#pragma mark - FSTView
+
+/**
+ * View is responsible for computing the final merged truth of what docs are in a query. It gets
+ * notified of local and remote changes to docs, and applies the query filters and limits to
+ * determine the most correct possible results.
+ */
+@interface FSTView : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ remoteDocuments:(FSTDocumentKeySet *)remoteDocuments NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Iterates over a set of doc changes, applies the query limit, and computes what the new results
+ * should be, what the changes were, and whether we may need to go back to the local cache for
+ * more results. Does not make any changes to the view.
+ *
+ * @param docChanges The doc changes to apply to this view.
+ * @return a new set of docs, changes, and refill flag.
+ */
+- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges;
+
+/**
+ * Iterates over a set of doc changes, applies the query limit, and computes what the new results
+ * should be, what the changes were, and whether we may need to go back to the local cache for
+ * more results. Does not make any changes to the view.
+ *
+ * @param docChanges The doc changes to apply to this view.
+ * @param previousChanges If this is being called with a refill, then start with this set of docs
+ * and changes instead of the current view.
+ * @return a new set of docs, changes, and refill flag.
+ */
+- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges
+ previousChanges:
+ (nullable FSTViewDocumentChanges *)previousChanges;
+
+/**
+ * Updates the view with the given ViewDocumentChanges.
+ *
+ * @param docChanges The set of changes to make to the view's docs.
+ * @return A new FSTViewChange with the given docs, changes, and sync state.
+ */
+- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges;
+
+/**
+ * Updates the view with the given FSTViewDocumentChanges and updates limbo docs and sync state from
+ * the given (optional) target change.
+ *
+ * @param docChanges The set of changes to make to the view's docs.
+ * @param targetChange A target change to apply for computing limbo docs and sync state.
+ * @return A new FSTViewChange with the given docs, changes, and sync state.
+ */
+- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges
+ targetChange:(nullable FSTTargetChange *)targetChange;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTView.m b/Firestore/Source/Core/FSTView.m
new file mode 100644
index 0000000..719e303
--- /dev/null
+++ b/Firestore/Source/Core/FSTView.m
@@ -0,0 +1,451 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTView.h"
+
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTDocumentSet.h"
+#import "FSTFieldValue.h"
+#import "FSTQuery.h"
+#import "FSTRemoteEvent.h"
+#import "FSTViewSnapshot.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTViewDocumentChanges
+
+/** The result of applying a set of doc changes to a view. */
+@interface FSTViewDocumentChanges ()
+
+- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet
+ changeSet:(FSTDocumentViewChangeSet *)changeSet
+ needsRefill:(BOOL)needsRefill
+ mutatedKeys:(FSTDocumentKeySet *)mutatedKeys NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FSTViewDocumentChanges
+
+- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet
+ changeSet:(FSTDocumentViewChangeSet *)changeSet
+ needsRefill:(BOOL)needsRefill
+ mutatedKeys:(FSTDocumentKeySet *)mutatedKeys {
+ self = [super init];
+ if (self) {
+ _documentSet = documentSet;
+ _changeSet = changeSet;
+ _needsRefill = needsRefill;
+ _mutatedKeys = mutatedKeys;
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - FSTLimboDocumentChange
+
+@interface FSTLimboDocumentChange ()
+
++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key;
+
+- (instancetype)initWithType:(FSTLimboDocumentChangeType)type
+ key:(FSTDocumentKey *)key NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FSTLimboDocumentChange
+
++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key {
+ return [[FSTLimboDocumentChange alloc] initWithType:type key:key];
+}
+
+- (instancetype)initWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key {
+ self = [super init];
+ if (self) {
+ _type = type;
+ _key = key;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTLimboDocumentChange class]]) {
+ return NO;
+ }
+ FSTLimboDocumentChange *otherChange = (FSTLimboDocumentChange *)other;
+ return self.type == otherChange.type && [self.key isEqual:otherChange.key];
+}
+
+@end
+
+#pragma mark - FSTViewChange
+
+@interface FSTViewChange ()
+
++ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot
+ limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges;
+
+- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot
+ limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges
+ NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FSTViewChange
+
++ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot
+ limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges {
+ return [[self alloc] initWithSnapshot:snapshot limboChanges:limboChanges];
+}
+
+- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot
+ limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges {
+ self = [super init];
+ if (self) {
+ _snapshot = snapshot;
+ _limboChanges = limboChanges;
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - FSTView
+
+static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1,
+ FSTDocumentViewChangeType c2);
+
+@interface FSTView ()
+
+@property(nonatomic, strong, readonly) FSTQuery *query;
+
+@property(nonatomic, assign) FSTSyncState syncState;
+
+/**
+ * A flag whether the view is current with the backend. A view is considered current after it
+ * has seen the current flag from the backend and did not lose consistency within the watch stream
+ * (e.g. because of an existence filter mismatch).
+ */
+@property(nonatomic, assign, getter=isCurrent) BOOL current;
+
+@property(nonatomic, strong) FSTDocumentSet *documentSet;
+
+/** Documents included in the remote target. */
+@property(nonatomic, strong) FSTDocumentKeySet *syncedDocuments;
+
+/** Documents in the view but not in the remote target */
+@property(nonatomic, strong) FSTDocumentKeySet *limboDocuments;
+
+/** Document Keys that have local changes. */
+@property(nonatomic, strong) FSTDocumentKeySet *mutatedKeys;
+
+@end
+
+@implementation FSTView
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ remoteDocuments:(nonnull FSTDocumentKeySet *)remoteDocuments {
+ self = [super init];
+ if (self) {
+ _query = query;
+ _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator];
+ _syncedDocuments = remoteDocuments;
+ _limboDocuments = [FSTDocumentKeySet keySet];
+ _mutatedKeys = [FSTDocumentKeySet keySet];
+ }
+ return self;
+}
+
+- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges {
+ return [self computeChangesWithDocuments:docChanges previousChanges:nil];
+}
+
+- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges
+ previousChanges:
+ (nullable FSTViewDocumentChanges *)previousChanges {
+ FSTDocumentViewChangeSet *changeSet =
+ previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet];
+ FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet;
+
+ __block FSTDocumentKeySet *newMutatedKeys =
+ previousChanges ? previousChanges.mutatedKeys : self.mutatedKeys;
+ __block FSTDocumentSet *newDocumentSet = oldDocumentSet;
+ __block BOOL needsRefill = NO;
+
+ // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an
+ // update moving a doc past the old limit) might mean there is some other document in the local
+ // cache that either should come (1) between the old last limit doc and the new last document,
+ // in the case of updates, or (2) after the new last document, in the case of deletes. So we
+ // keep this doc at the old limit to compare the updates to.
+ //
+ // Note that this should never get used in a refill (when previousChanges is set), because there
+ // will only be adds -- no deletes or updates.
+ FSTDocument *_Nullable lastDocInLimit =
+ (self.query.limit && oldDocumentSet.count == self.query.limit) ? oldDocumentSet.lastDocument
+ : nil;
+
+ [docChanges enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key,
+ FSTMaybeDocument *maybeNewDoc, BOOL *stop) {
+ FSTDocument *_Nullable oldDoc = [oldDocumentSet documentForKey:key];
+ FSTDocument *_Nullable newDoc = nil;
+ if ([maybeNewDoc isKindOfClass:[FSTDocument class]]) {
+ newDoc = (FSTDocument *)maybeNewDoc;
+ }
+ if (newDoc) {
+ FSTAssert([key isEqual:newDoc.key], @"Mismatching key in document changes: %@ != %@", key,
+ newDoc.key);
+ if (![self.query matchesDocument:newDoc]) {
+ newDoc = nil;
+ }
+ }
+ if (newDoc) {
+ newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc];
+ if (newDoc.hasLocalMutations) {
+ newMutatedKeys = [newMutatedKeys setByAddingObject:key];
+ } else {
+ newMutatedKeys = [newMutatedKeys setByRemovingObject:key];
+ }
+ } else {
+ newDocumentSet = [newDocumentSet documentSetByRemovingKey:key];
+ newMutatedKeys = [newMutatedKeys setByRemovingObject:key];
+ }
+
+ // Calculate change
+ if (oldDoc && newDoc) {
+ BOOL docsEqual = [oldDoc.data isEqual:newDoc.data];
+ if (!docsEqual || oldDoc.hasLocalMutations != newDoc.hasLocalMutations) {
+ // only report a change if document actually changed.
+ if (docsEqual) {
+ [changeSet addChange:[FSTDocumentViewChange
+ changeWithDocument:newDoc
+ type:FSTDocumentViewChangeTypeMetadata]];
+ } else {
+ [changeSet addChange:[FSTDocumentViewChange
+ changeWithDocument:newDoc
+ type:FSTDocumentViewChangeTypeModified]];
+ }
+
+ if (lastDocInLimit && self.query.comparator(newDoc, lastDocInLimit) > 0) {
+ // This doc moved from inside the limit to after the limit. That means there may be some
+ // doc in the local cache that's actually less than this one.
+ needsRefill = YES;
+ }
+ }
+ } else if (!oldDoc && newDoc) {
+ [changeSet
+ addChange:[FSTDocumentViewChange changeWithDocument:newDoc
+ type:FSTDocumentViewChangeTypeAdded]];
+ } else if (oldDoc && !newDoc) {
+ [changeSet
+ addChange:[FSTDocumentViewChange changeWithDocument:oldDoc
+ type:FSTDocumentViewChangeTypeRemoved]];
+ if (lastDocInLimit) {
+ // A doc was removed from a full limit query. We'll need to re-query from the local cache
+ // to see if we know about some other doc that should be in the results.
+ needsRefill = YES;
+ }
+ }
+ }];
+ if (self.query.limit) {
+ // TODO(klimt): Make DocumentSet size be constant time.
+ while (newDocumentSet.count > self.query.limit) {
+ FSTDocument *oldDoc = [newDocumentSet lastDocument];
+ newDocumentSet = [newDocumentSet documentSetByRemovingKey:oldDoc.key];
+ [changeSet
+ addChange:[FSTDocumentViewChange changeWithDocument:oldDoc
+ type:FSTDocumentViewChangeTypeRemoved]];
+ }
+ }
+
+ FSTAssert(!needsRefill || !previousChanges,
+ @"View was refilled using docs that themselves needed refilling.");
+
+ return [[FSTViewDocumentChanges alloc] initWithDocumentSet:newDocumentSet
+ changeSet:changeSet
+ needsRefill:needsRefill
+ mutatedKeys:newMutatedKeys];
+}
+
+- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges {
+ return [self applyChangesToDocuments:docChanges targetChange:nil];
+}
+
+- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges
+ targetChange:(nullable FSTTargetChange *)targetChange {
+ FSTAssert(!docChanges.needsRefill, @"Cannot apply changes that need a refill");
+
+ FSTDocumentSet *oldDocuments = self.documentSet;
+ self.documentSet = docChanges.documentSet;
+ self.mutatedKeys = docChanges.mutatedKeys;
+
+ // Sort changes based on type and query comparator.
+ NSArray<FSTDocumentViewChange *> *changes = [docChanges.changeSet changes];
+ changes = [changes sortedArrayUsingComparator:^NSComparisonResult(FSTDocumentViewChange *c1,
+ FSTDocumentViewChange *c2) {
+ NSComparisonResult typeComparison = FSTCompareDocumentViewChangeTypes(c1.type, c2.type);
+ if (typeComparison != NSOrderedSame) {
+ return typeComparison;
+ }
+ return self.query.comparator(c1.document, c2.document);
+ }];
+
+ NSArray<FSTLimboDocumentChange *> *limboChanges = [self applyTargetChange:targetChange];
+ BOOL synced = self.limboDocuments.count == 0 && self.isCurrent;
+ FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal;
+ BOOL syncStateChanged = newSyncState != self.syncState;
+ self.syncState = newSyncState;
+
+ if (changes.count == 0 && !syncStateChanged) {
+ // No changes.
+ return [FSTViewChange changeWithSnapshot:nil limboChanges:limboChanges];
+ } else {
+ FSTViewSnapshot *snapshot =
+ [[FSTViewSnapshot alloc] initWithQuery:self.query
+ documents:docChanges.documentSet
+ oldDocuments:oldDocuments
+ documentChanges:changes
+ fromCache:newSyncState == FSTSyncStateLocal
+ hasPendingWrites:!docChanges.mutatedKeys.isEmpty
+ syncStateChanged:syncStateChanged];
+
+ return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges];
+ }
+}
+
+#pragma mark - Private methods
+
+/** Returns whether the doc for the given key should be in limbo. */
+- (BOOL)shouldBeLimboDocumentKey:(FSTDocumentKey *)key {
+ // If the remote end says it's part of this query, it's not in limbo.
+ if ([self.syncedDocuments containsObject:key]) {
+ return NO;
+ }
+ // The local store doesn't think it's a result, so it shouldn't be in limbo.
+ if (![self.documentSet containsKey:key]) {
+ return NO;
+ }
+ // If there are local changes to the doc, they might explain why the server doesn't know that it's
+ // part of the query. So don't put it in limbo.
+ // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific
+ // query.
+ if ([self.documentSet documentForKey:key].hasLocalMutations) {
+ return NO;
+ }
+ // Everything else is in limbo.
+ return YES;
+}
+
+/**
+ * Updates syncedDocuments, isAcked, and limbo docs based on the given change.
+ * @return the list of changes to which docs are in limbo.
+ */
+- (NSArray<FSTLimboDocumentChange *> *)applyTargetChange:(nullable FSTTargetChange *)targetChange {
+ if (targetChange) {
+ FSTTargetMapping *targetMapping = targetChange.mapping;
+ if ([targetMapping isKindOfClass:[FSTResetMapping class]]) {
+ self.syncedDocuments = ((FSTResetMapping *)targetMapping).documents;
+ } else if ([targetMapping isKindOfClass:[FSTUpdateMapping class]]) {
+ [((FSTUpdateMapping *)targetMapping).addedDocuments
+ enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ self.syncedDocuments = [self.syncedDocuments setByAddingObject:key];
+ }];
+ [((FSTUpdateMapping *)targetMapping).removedDocuments
+ enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ self.syncedDocuments = [self.syncedDocuments setByRemovingObject:key];
+ }];
+ }
+
+ switch (targetChange.currentStatusUpdate) {
+ case FSTCurrentStatusUpdateMarkCurrent:
+ self.current = YES;
+ break;
+ case FSTCurrentStatusUpdateMarkNotCurrent:
+ self.current = NO;
+ break;
+ case FSTCurrentStatusUpdateNone:
+ break;
+ }
+ }
+
+ // Recompute the set of limbo docs.
+ // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents.
+ FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments;
+ self.limboDocuments = [FSTDocumentKeySet keySet];
+ if (self.isCurrent) {
+ for (FSTDocument *doc in self.documentSet.documentEnumerator) {
+ if ([self shouldBeLimboDocumentKey:doc.key]) {
+ self.limboDocuments = [self.limboDocuments setByAddingObject:doc.key];
+ }
+ }
+ }
+
+ // Diff the new limbo docs with the old limbo docs.
+ NSMutableArray<FSTLimboDocumentChange *> *changes =
+ [NSMutableArray arrayWithCapacity:(oldLimboDocuments.count + self.limboDocuments.count)];
+ [oldLimboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ if (![self.limboDocuments containsObject:key]) {
+ [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved
+ key:key]];
+ }
+ }];
+ [self.limboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ if (![oldLimboDocuments containsObject:key]) {
+ [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded
+ key:key]];
+ }
+ }];
+ return changes;
+}
+
+@end
+
+static inline int DocumentViewChangeTypePosition(FSTDocumentViewChangeType changeType) {
+ switch (changeType) {
+ case FSTDocumentViewChangeTypeRemoved:
+ return 0;
+ case FSTDocumentViewChangeTypeAdded:
+ return 1;
+ case FSTDocumentViewChangeTypeModified:
+ return 2;
+ case FSTDocumentViewChangeTypeMetadata:
+ // A metadata change is converted to a modified change at the public API layer. Since we sort
+ // by document key and then change type, metadata and modified changes must be sorted
+ // equivalently.
+ return 2;
+ default:
+ FSTCFail(@"Unknown FSTDocumentViewChangeType %lu", (unsigned long)changeType);
+ }
+}
+
+static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1,
+ FSTDocumentViewChangeType c2) {
+ int pos1 = DocumentViewChangeTypePosition(c1);
+ int pos2 = DocumentViewChangeTypePosition(c2);
+ if (pos1 == pos2) {
+ return NSOrderedSame;
+ } else if (pos1 < pos2) {
+ return NSOrderedAscending;
+ } else {
+ return NSOrderedDescending;
+ }
+}
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTViewSnapshot.h b/Firestore/Source/Core/FSTViewSnapshot.h
new file mode 100644
index 0000000..3db6108
--- /dev/null
+++ b/Firestore/Source/Core/FSTViewSnapshot.h
@@ -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 <Foundation/Foundation.h>
+
+@class FSTDocument;
+@class FSTQuery;
+@class FSTDocumentSet;
+@class FSTViewSnapshot;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTDocumentViewChange
+
+/**
+ * The types of changes that can happen to a document with respect to a view.
+ * NOTE: We sort document changes by their type, so the ordering of this enum is significant.
+ */
+typedef NS_ENUM(NSInteger, FSTDocumentViewChangeType) {
+ FSTDocumentViewChangeTypeRemoved = 0,
+ FSTDocumentViewChangeTypeAdded,
+ FSTDocumentViewChangeTypeModified,
+ FSTDocumentViewChangeTypeMetadata,
+};
+
+/** A change to a single document's state within a view. */
+@interface FSTDocumentViewChange : NSObject
+
+- (id)init __attribute__((unavailable("Use a static constructor method.")));
+
++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type;
+
+/** The type of change for the document. */
+@property(nonatomic, assign, readonly) FSTDocumentViewChangeType type;
+/** The document whose status changed. */
+@property(nonatomic, strong, readonly) FSTDocument *document;
+
+@end
+
+#pragma mark - FSTDocumentChangeSet
+
+/** The possibly states a document can be in w.r.t syncing from local storage to the backend. */
+typedef NS_ENUM(NSInteger, FSTSyncState) {
+ FSTSyncStateNone = 0,
+ FSTSyncStateLocal,
+ FSTSyncStateSynced,
+};
+
+/** A set of changes to documents with respect to a view. This set is mutable. */
+@interface FSTDocumentViewChangeSet : NSObject
+
+/** Returns a new empty change set. */
++ (instancetype)changeSet;
+
+/** Takes a new change and applies it to the set. */
+- (void)addChange:(FSTDocumentViewChange *)change;
+
+/** Returns the set of all changes tracked in this set. */
+- (NSArray<FSTDocumentViewChange *> *)changes;
+
+@end
+
+#pragma mark - FSTViewSnapshot
+
+typedef void (^FSTViewSnapshotHandler)(FSTViewSnapshot *_Nullable snapshot,
+ NSError *_Nullable error);
+
+/** A view snapshot is an immutable capture of the results of a query and the changes to them. */
+@interface FSTViewSnapshot : NSObject
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ documents:(FSTDocumentSet *)documents
+ oldDocuments:(FSTDocumentSet *)oldDocuments
+ documentChanges:(NSArray<FSTDocumentViewChange *> *)documentChanges
+ fromCache:(BOOL)fromCache
+ hasPendingWrites:(BOOL)hasPendingWrites
+ syncStateChanged:(BOOL)syncStateChanged NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** The query this view is tracking the results for. */
+@property(nonatomic, strong, readonly) FSTQuery *query;
+
+/** The documents currently known to be results of the query. */
+@property(nonatomic, strong, readonly) FSTDocumentSet *documents;
+
+/** The documents of the last snapshot. */
+@property(nonatomic, strong, readonly) FSTDocumentSet *oldDocuments;
+
+/** The set of changes that have been applied to the documents. */
+@property(nonatomic, strong, readonly) NSArray<FSTDocumentViewChange *> *documentChanges;
+
+/** Whether any document in the snapshot was served from the local cache. */
+@property(nonatomic, assign, readonly, getter=isFromCache) BOOL fromCache;
+
+/** Whether any document in the snapshot has pending local writes. */
+@property(nonatomic, assign, readonly) BOOL hasPendingWrites;
+
+/** Whether the sync state changed as part of this snapshot. */
+@property(nonatomic, assign, readonly) BOOL syncStateChanged;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Core/FSTViewSnapshot.m b/Firestore/Source/Core/FSTViewSnapshot.m
new file mode 100644
index 0000000..016f890
--- /dev/null
+++ b/Firestore/Source/Core/FSTViewSnapshot.m
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTViewSnapshot.h"
+
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTDocumentSet.h"
+#import "FSTImmutableSortedDictionary.h"
+#import "FSTQuery.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTDocumentViewChange
+
+@interface FSTDocumentViewChange ()
+
++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type;
+
+- (instancetype)initWithDocument:(FSTDocument *)document
+ type:(FSTDocumentViewChangeType)type NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FSTDocumentViewChange
+
++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type {
+ return [[FSTDocumentViewChange alloc] initWithDocument:document type:type];
+}
+
+- (instancetype)initWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type {
+ self = [super init];
+ if (self) {
+ _document = document;
+ _type = type;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTDocumentViewChange class]]) {
+ return NO;
+ }
+ FSTDocumentViewChange *otherChange = (FSTDocumentViewChange *)other;
+ return [self.document isEqual:otherChange.document] && self.type == otherChange.type;
+}
+
+- (NSString *)description {
+ return [NSString
+ stringWithFormat:@"<FSTDocumentViewChange type:%ld doc:%@>", (long)self.type, self.document];
+}
+
+@end
+
+#pragma mark - FSTDocumentViewChangeSet
+
+@interface FSTDocumentViewChangeSet ()
+
+/** The set of all changes tracked so far, with redundant changes merged. */
+@property(nonatomic, strong)
+ FSTImmutableSortedDictionary<FSTDocumentKey *, FSTDocumentViewChange *> *changeMap;
+
+@end
+
+@implementation FSTDocumentViewChangeSet
+
++ (instancetype)changeSet {
+ return [[FSTDocumentViewChangeSet alloc] init];
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _changeMap = [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator];
+ }
+ return self;
+}
+
+- (NSString *)description {
+ return [self.changeMap description];
+}
+
+- (void)addChange:(FSTDocumentViewChange *)change {
+ FSTDocumentKey *key = change.document.key;
+ FSTDocumentViewChange *oldChange = [self.changeMap objectForKey:key];
+ if (!oldChange) {
+ self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key];
+ return;
+ }
+
+ // Merge the new change with the existing change.
+ if (change.type != FSTDocumentViewChangeTypeAdded &&
+ oldChange.type == FSTDocumentViewChangeTypeMetadata) {
+ self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key];
+
+ } else if (change.type == FSTDocumentViewChangeTypeMetadata &&
+ oldChange.type != FSTDocumentViewChangeTypeRemoved) {
+ FSTDocumentViewChange *newChange =
+ [FSTDocumentViewChange changeWithDocument:change.document type:oldChange.type];
+ self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key];
+
+ } else if (change.type == FSTDocumentViewChangeTypeModified &&
+ oldChange.type == FSTDocumentViewChangeTypeModified) {
+ FSTDocumentViewChange *newChange =
+ [FSTDocumentViewChange changeWithDocument:change.document
+ type:FSTDocumentViewChangeTypeModified];
+ self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key];
+ } else if (change.type == FSTDocumentViewChangeTypeModified &&
+ oldChange.type == FSTDocumentViewChangeTypeAdded) {
+ FSTDocumentViewChange *newChange =
+ [FSTDocumentViewChange changeWithDocument:change.document
+ type:FSTDocumentViewChangeTypeAdded];
+ self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key];
+ } else if (change.type == FSTDocumentViewChangeTypeRemoved &&
+ oldChange.type == FSTDocumentViewChangeTypeAdded) {
+ self.changeMap = [self.changeMap dictionaryByRemovingObjectForKey:key];
+ } else if (change.type == FSTDocumentViewChangeTypeRemoved &&
+ oldChange.type == FSTDocumentViewChangeTypeModified) {
+ FSTDocumentViewChange *newChange =
+ [FSTDocumentViewChange changeWithDocument:oldChange.document
+ type:FSTDocumentViewChangeTypeRemoved];
+ self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key];
+ } else if (change.type == FSTDocumentViewChangeTypeAdded &&
+ oldChange.type == FSTDocumentViewChangeTypeRemoved) {
+ FSTDocumentViewChange *newChange =
+ [FSTDocumentViewChange changeWithDocument:change.document
+ type:FSTDocumentViewChangeTypeModified];
+ self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key];
+ } else {
+ // This includes these cases, which don't make sense:
+ // Added -> Added
+ // Removed -> Removed
+ // Modified -> Added
+ // Removed -> Modified
+ // Metadata -> Added
+ // Removed -> Metadata
+ FSTFail(@"Unsupported combination of changes: %ld after %ld", (long)change.type,
+ (long)oldChange.type);
+ }
+}
+
+- (NSArray<FSTDocumentViewChange *> *)changes {
+ NSMutableArray<FSTDocumentViewChange *> *changes = [NSMutableArray array];
+ [self.changeMap enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key,
+ FSTDocumentViewChange *change, BOOL *stop) {
+ [changes addObject:change];
+ }];
+ return changes;
+}
+
+@end
+
+#pragma mark - FSTViewSnapshot
+
+@implementation FSTViewSnapshot
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ documents:(FSTDocumentSet *)documents
+ oldDocuments:(FSTDocumentSet *)oldDocuments
+ documentChanges:(NSArray<FSTDocumentViewChange *> *)documentChanges
+ fromCache:(BOOL)fromCache
+ hasPendingWrites:(BOOL)hasPendingWrites
+ syncStateChanged:(BOOL)syncStateChanged {
+ self = [super init];
+ if (self) {
+ _query = query;
+ _documents = documents;
+ _oldDocuments = oldDocuments;
+ _documentChanges = documentChanges;
+ _fromCache = fromCache;
+ _hasPendingWrites = hasPendingWrites;
+ _syncStateChanged = syncStateChanged;
+ }
+ return self;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:
+ @"<FSTViewSnapshot query:%@ documents:%@ oldDocument:%@ changes:%@ "
+ "fromCache:%@ hasPendingWrites:%@ syncStateChanged:%@>",
+ self.query, self.documents, self.oldDocuments, self.documentChanges,
+ (self.fromCache ? @"YES" : @"NO"), (self.hasPendingWrites ? @"YES" : @"NO"),
+ (self.syncStateChanged ? @"YES" : @"NO")];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ } else if (![object isKindOfClass:[FSTViewSnapshot class]]) {
+ return NO;
+ }
+
+ FSTViewSnapshot *other = object;
+ return [self.query isEqual:other.query] && [self.documents isEqual:other.documents] &&
+ [self.oldDocuments isEqual:other.oldDocuments] &&
+ [self.documentChanges isEqualToArray:other.documentChanges] &&
+ self.fromCache == other.fromCache && self.hasPendingWrites == other.hasPendingWrites &&
+ self.syncStateChanged == other.syncStateChanged;
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.query hash];
+ result = 31 * result + [self.documents hash];
+ result = 31 * result + [self.oldDocuments hash];
+ result = 31 * result + [self.documentChanges hash];
+ result = 31 * result + (self.fromCache ? 1231 : 1237);
+ result = 31 * result + (self.hasPendingWrites ? 1231 : 1237);
+ result = 31 * result + (self.syncStateChanged ? 1231 : 1237);
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTDocumentReference.h b/Firestore/Source/Local/FSTDocumentReference.h
new file mode 100644
index 0000000..eff60e4
--- /dev/null
+++ b/Firestore/Source/Local/FSTDocumentReference.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 FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An immutable value used to keep track of an association between some referencing target or batch
+ * and a document key that the target or batch references.
+ *
+ * A reference can be from either listen targets (identified by their FSTTargetID) or mutation
+ * batches (identified by their FSTBatchID). See FSTGarbageCollector for more details.
+ *
+ * Not to be confused with FIRDocumentReference.
+ */
+@interface FSTDocumentReference : NSObject <NSCopying>
+
+/** Initializes the document reference with the given key and ID. */
+- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int)ID NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** The document key that's the target of this reference. */
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+
+/**
+ * The targetID of a referring target or the batchID of a referring mutation batch. (Which this
+ * is depends upon which FSTReferenceSet this reference is a part of.)
+ */
+@property(nonatomic, assign, readonly) int ID;
+
+@end
+
+#pragma mark Comparators
+
+/** Sorts document references by key then ID. */
+extern const NSComparator FSTDocumentReferenceComparatorByKey;
+
+/** Sorts document references by ID then key. */
+extern const NSComparator FSTDocumentReferenceComparatorByID;
+
+/** A callback for use when enumerating an FSTImmutableSortedSet of FSTDocumentReferences. */
+typedef void (^FSTDocumentReferenceBlock)(FSTDocumentReference *reference, BOOL *stop);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTDocumentReference.m b/Firestore/Source/Local/FSTDocumentReference.m
new file mode 100644
index 0000000..7d9e3db
--- /dev/null
+++ b/Firestore/Source/Local/FSTDocumentReference.m
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTDocumentReference.h"
+
+#import "FSTComparison.h"
+#import "FSTDocumentKey.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTDocumentReference
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int)ID {
+ self = [super init];
+ if (self) {
+ _key = key;
+ _ID = ID;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (!other || ![[other class] isEqual:[self class]]) return NO;
+
+ FSTDocumentReference *reference = (FSTDocumentReference *)other;
+
+ return [self.key isEqualToKey:reference.key] && self.ID == reference.ID;
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.key hash];
+ result = result * 31u + self.ID;
+ return result;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTDocumentReference: key=%@, ID=%d>", self.key, self.ID];
+}
+
+- (id)copyWithZone:(nullable NSZone *)zone {
+ // FSTDocumentReference is immutable
+ return self;
+}
+
+@end
+
+#pragma mark Comparators
+
+/** Sorts document references by key then ID. */
+const NSComparator FSTDocumentReferenceComparatorByKey =
+ ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) {
+ NSComparisonResult result = FSTDocumentKeyComparator(left.key, right.key);
+ if (result != NSOrderedSame) {
+ return result;
+ }
+ return FSTCompareInts(left.ID, right.ID);
+ };
+
+/** Sorts document references by ID then key. */
+const NSComparator FSTDocumentReferenceComparatorByID =
+ ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) {
+ NSComparisonResult result = FSTCompareInts(left.ID, right.ID);
+ if (result != NSOrderedSame) {
+ return result;
+ }
+ return FSTDocumentKeyComparator(left.key, right.key);
+ };
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTEagerGarbageCollector.h b/Firestore/Source/Local/FSTEagerGarbageCollector.h
new file mode 100644
index 0000000..f2f373c
--- /dev/null
+++ b/Firestore/Source/Local/FSTEagerGarbageCollector.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 "FSTGarbageCollector.h"
+
+@class FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A garbage collector implementation that eagerly collects documents as soon as they're no longer
+ * referenced in any of its registered FSTGarbageSources.
+ *
+ * This implementation keeps track of a set of keys that are potentially garbage without keeping
+ * an exact reference count. During -collectGarbage, the collector verifies that all potential
+ * garbage keys actually have no references by consulting its list of garbage sources.
+ */
+@interface FSTEagerGarbageCollector : NSObject <FSTGarbageCollector>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTEagerGarbageCollector.m b/Firestore/Source/Local/FSTEagerGarbageCollector.m
new file mode 100644
index 0000000..971f368
--- /dev/null
+++ b/Firestore/Source/Local/FSTEagerGarbageCollector.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 "FSTEagerGarbageCollector.h"
+
+#import "FSTDocumentKey.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTMultiReferenceSet
+
+@interface FSTEagerGarbageCollector ()
+
+/** The garbage collectible sources to double-check during garbage collection. */
+@property(nonatomic, strong, readonly) NSMutableArray<id<FSTGarbageSource>> *sources;
+
+/** A set of potentially garbage keys. */
+@property(nonatomic, strong, readonly) NSMutableSet<FSTDocumentKey *> *potentialGarbage;
+
+@end
+
+@implementation FSTEagerGarbageCollector
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _sources = [NSMutableArray array];
+ _potentialGarbage = [[NSMutableSet alloc] init];
+ }
+ return self;
+}
+
+- (BOOL)isEager {
+ return YES;
+}
+
+- (void)addGarbageSource:(id<FSTGarbageSource>)garbageSource {
+ [self.sources addObject:garbageSource];
+ garbageSource.garbageCollector = self;
+}
+
+- (void)removeGarbageSource:(id<FSTGarbageSource>)garbageSource {
+ [self.sources removeObject:garbageSource];
+ garbageSource.garbageCollector = nil;
+}
+
+- (void)addPotentialGarbageKey:(FSTDocumentKey *)key {
+ [self.potentialGarbage addObject:key];
+}
+
+- (NSMutableSet<FSTDocumentKey *> *)collectGarbage {
+ NSMutableArray<id<FSTGarbageSource>> *sources = self.sources;
+
+ NSMutableSet<FSTDocumentKey *> *actualGarbage = [NSMutableSet set];
+ for (FSTDocumentKey *key in self.potentialGarbage) {
+ BOOL isGarbage = YES;
+ for (id<FSTGarbageSource> source in sources) {
+ if ([source containsKey:key]) {
+ isGarbage = NO;
+ break;
+ }
+ }
+
+ if (isGarbage) {
+ [actualGarbage addObject:key];
+ }
+ }
+
+ // Clear locally retained potential keys and returned confirmed garbage.
+ [self.potentialGarbage removeAllObjects];
+ return actualGarbage;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTGarbageCollector.h b/Firestore/Source/Local/FSTGarbageCollector.h
new file mode 100644
index 0000000..c999f66
--- /dev/null
+++ b/Firestore/Source/Local/FSTGarbageCollector.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTTypes.h"
+
+@class FSTDocumentKey;
+@class FSTDocumentReference;
+@protocol FSTGarbageCollector;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A pseudo-collection that maintains references to documents. FSTGarbageSource collections
+ * notify the FSTGarbageCollector when references to documents change through the
+ * -addPotentialGarbageKey: message.
+ */
+@protocol FSTGarbageSource
+
+/**
+ * The garbage collector to which this collection should send -addPotentialGarbageKey: messages.
+ */
+@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector;
+
+/**
+ * Checks to see if there are any references to a document with the given key. This can be used by
+ * garbage collectors to double-check if a key exists in this collection when it was released
+ * elsewhere.
+ */
+- (BOOL)containsKey:(FSTDocumentKey *)key;
+
+@end
+
+/**
+ * Tracks different kinds of references to a document, for all the different ways the client
+ * needs to retain a document.
+ *
+ * Usually the local store this means tracking of three different types of references to a
+ * document:
+ * 1. RemoteTarget reference identified by a target ID.
+ * 2. LocalView reference identified also by a target ID.
+ * 3. Local mutation reference identified by a batch ID.
+ *
+ * The idea is that we want to keep a document around at least as long as any remote target or
+ * local (latency compensated) view is referencing it, or there's an outstanding local mutation to
+ * that document.
+ */
+@protocol FSTGarbageCollector
+
+/**
+ * A property that describes whether or not the collector wants to eagerly collect keys.
+ *
+ * TODO(b/33384523) Delegate deleting released queries to the GC.
+ * This flag is a temporary workaround for dealing with a persistent query cache. The collector
+ * really should have an API for releasing queries that does the right thing for its policy.
+ */
+@property(nonatomic, assign, readonly, getter=isEager) BOOL eager;
+
+/** Adds a garbage source to the collector. */
+- (void)addGarbageSource:(id<FSTGarbageSource>)garbageSource;
+
+/** Removes a garbage source from the collector. */
+- (void)removeGarbageSource:(id<FSTGarbageSource>)garbageSource;
+
+/**
+ * Notifies the garbage collector that a document with the given key may have become garbage.
+ *
+ * This is useful in both when a document has definitely been released (for example when removed
+ * from a garbage source) but also when a document has been updated. Documents should be marked in
+ * this way because the client accepts updates for documents even after the document no longer
+ * matches any active targets. This behavior allows the client to avoid re-showing an old document
+ * in the next latency-compensated view.
+ */
+- (void)addPotentialGarbageKey:(FSTDocumentKey *)key;
+
+/** Returns the contents of the garbage bin and clears it. */
+- (NSSet<FSTDocumentKey *> *)collectGarbage;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDB.h b/Firestore/Source/Local/FSTLevelDB.h
new file mode 100644
index 0000000..a2c838d
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDB.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTPersistence.h"
+
+#ifdef __cplusplus
+#include <memory>
+
+namespace leveldb {
+class DB;
+class Status;
+}
+#endif
+
+@class FSTDatabaseInfo;
+@class FSTLocalSerializer;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** A LevelDB-backed instance of FSTPersistence. */
+// TODO(mikelehen): Rename to FSTLevelDBPersistence.
+@interface FSTLevelDB : NSObject <FSTPersistence>
+
+/**
+ * Initializes the LevelDB in the given directory. Note that all expensive startup work including
+ * opening any database files is deferred until -[FSTPersistence start] is called.
+ */
+- (instancetype)initWithDirectory:(NSString *)directory
+ serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init __attribute__((unavailable("Use -initWithDirectory: instead.")));
+
+/** Finds a suitable directory to serve as the root of all Firestore local storage. */
++ (NSString *)documentsDirectory;
+
+/**
+ * Computes a unique storage directory for the given identifying components of local storage.
+ *
+ * @param databaseInfo The identifying information for the local storage instance.
+ * @param documentsDirectory The root document directory relative to which the storage directory
+ * will be created. Usually just +[FSTLevelDB documentsDir].
+ * @return A storage directory unique to the instance identified by databaseInfo.
+ */
++ (NSString *)storageDirectoryForDatabaseInfo:(FSTDatabaseInfo *)databaseInfo
+ documentsDirectory:(NSString *)documentsDirectory;
+
+/**
+ * Starts LevelDB-backed persistent storage by opening the database files, creating the DB if it
+ * does not exist.
+ *
+ * The leveldb directory is created relative to the appropriate document storage directory for the
+ * platform: NSDocumentDirectory on iOS or $HOME/.firestore on macOS.
+ */
+- (BOOL)start:(NSError **)error;
+
+#ifdef __cplusplus
+// What follows is the Objective-C++ extension to the API.
+
+/**
+ * Creates an NSError based on the given status if the status is not ok.
+ *
+ * @param status The status of the preceding LevelDB operation.
+ * @param description A printf-style format string describing what kind of failure happened if
+ * @a status is not ok. Additional parameters are substituted into the placeholders in this
+ * format string.
+ *
+ * @return An NSError with its localizedDescription composed from the description format and its
+ * localizedFailureReason composed from any error message embedded in @a status.
+ */
++ (nullable NSError *)errorWithStatus:(leveldb::Status)status
+ description:(NSString *)description, ... NS_FORMAT_FUNCTION(2, 3);
+
+/**
+ * Converts the given @a status to an NSString describing the status condition, suitable for
+ * logging or inclusion in an NSError.
+ *
+ * @param status The status of the preceding LevelDB operation.
+ *
+ * @return An NSString describing the status (even if the status was ok).
+ */
++ (NSString *)descriptionOfStatus:(leveldb::Status)status;
+
+/** The native db pointer, allocated during start. */
+@property(nonatomic, assign, readonly) std::shared_ptr<leveldb::DB> ptr;
+
+#endif
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm
new file mode 100644
index 0000000..81e1064
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDB.mm
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTLevelDB.h"
+
+#include <leveldb/db.h>
+
+#import "FIRFirestoreErrors.h"
+#import "FSTAssert.h"
+#import "FSTDatabaseID.h"
+#import "FSTDatabaseInfo.h"
+#import "FSTLevelDBMutationQueue.h"
+#import "FSTLevelDBQueryCache.h"
+#import "FSTLevelDBRemoteDocumentCache.h"
+#import "FSTLogger.h"
+#import "FSTSerializerBeta.h"
+#import "FSTWriteGroup.h"
+#import "FSTWriteGroupTracker.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const kReservedPathComponent = @"firestore";
+
+using leveldb::DB;
+using leveldb::Options;
+using leveldb::Status;
+using leveldb::WriteOptions;
+
+@interface FSTLevelDB ()
+
+@property(nonatomic, copy) NSString *directory;
+@property(nonatomic, strong) FSTWriteGroupTracker *writeGroupTracker;
+@property(nonatomic, assign, getter=isStarted) BOOL started;
+@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer;
+
+@end
+
+@implementation FSTLevelDB
+
+- (instancetype)initWithDirectory:(NSString *)directory
+ serializer:(FSTLocalSerializer *)serializer {
+ if (self = [super init]) {
+ _directory = [directory copy];
+ _writeGroupTracker = [FSTWriteGroupTracker tracker];
+ _serializer = serializer;
+ }
+ return self;
+}
+
++ (NSString *)documentsDirectory {
+#if TARGET_OS_IPHONE
+ NSArray<NSString *> *directories =
+ NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ return [directories[0] stringByAppendingPathComponent:kReservedPathComponent];
+
+#elif TARGET_OS_MAC
+ NSString *dotPrefixed = [@"." stringByAppendingString:kReservedPathComponent];
+ return [NSHomeDirectory() stringByAppendingPathComponent:dotPrefixed];
+
+#else
+#error "local storage on tvOS"
+// TODO(mcg): Writing to NSDocumentsDirectory on tvOS will fail; we need to write to Caches
+// https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/
+
+#endif
+}
+
++ (NSString *)storageDirectoryForDatabaseInfo:(FSTDatabaseInfo *)databaseInfo
+ documentsDirectory:(NSString *)documentsDirectory {
+ // Use two different path formats:
+ //
+ // * persistenceKey / projectID . databaseID / name
+ // * persistenceKey / projectID / name
+ //
+ // projectIDs are DNS-compatible names and cannot contain dots so there's
+ // no danger of collisions.
+ NSString *directory = documentsDirectory;
+ directory = [directory stringByAppendingPathComponent:databaseInfo.persistenceKey];
+
+ NSString *segment = databaseInfo.databaseID.projectID;
+ if (![databaseInfo.databaseID isDefaultDatabase]) {
+ segment = [NSString stringWithFormat:@"%@.%@", segment, databaseInfo.databaseID.databaseID];
+ }
+ directory = [directory stringByAppendingPathComponent:segment];
+
+ // Reserve one additional path component to allow multiple physical databases
+ directory = [directory stringByAppendingPathComponent:@"main"];
+ return directory;
+}
+
+#pragma mark - Startup
+
+- (BOOL)start:(NSError **)error {
+ FSTAssert(!self.isStarted, @"FSTLevelDB double-started!");
+ self.started = YES;
+ NSString *directory = self.directory;
+ if (![self ensureDirectory:directory error:error]) {
+ return NO;
+ }
+
+ DB *database = [self createDBWithDirectory:directory error:error];
+ if (!database) {
+ return NO;
+ }
+
+ _ptr.reset(database);
+ return YES;
+}
+
+/** Creates the directory at @a directory and marks it as excluded from iCloud backup. */
+- (BOOL)ensureDirectory:(NSString *)directory error:(NSError **)error {
+ NSError *localError;
+ NSFileManager *files = [NSFileManager defaultManager];
+
+ BOOL success = [files createDirectoryAtPath:directory
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&localError];
+ if (!success) {
+ *error =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeInternal
+ userInfo:@{
+ NSLocalizedDescriptionKey : @"Failed to create persistence directory",
+ NSUnderlyingErrorKey : localError
+ }];
+ return NO;
+ }
+
+ NSURL *dirURL = [NSURL fileURLWithPath:directory];
+ success = [dirURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&localError];
+ if (!success) {
+ *error = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeInternal
+ userInfo:@{
+ NSLocalizedDescriptionKey :
+ @"Failed mark persistence directory as excluded from backups",
+ NSUnderlyingErrorKey : localError
+ }];
+ return NO;
+ }
+
+ return YES;
+}
+
+/** Opens the database within the given directory. */
+- (nullable DB *)createDBWithDirectory:(NSString *)directory error:(NSError **)error {
+ Options options;
+ options.create_if_missing = true;
+
+ DB *database;
+ Status status = DB::Open(options, [directory UTF8String], &database);
+ if (!status.ok()) {
+ if (error) {
+ NSString *name = [directory lastPathComponent];
+ *error =
+ [FSTLevelDB errorWithStatus:status
+ description:@"Failed to create database %@ at path %@", name, directory];
+ }
+ return nullptr;
+ }
+
+ return database;
+}
+
+#pragma mark - Persistence Factory methods
+
+- (id<FSTMutationQueue>)mutationQueueForUser:(FSTUser *)user {
+ return [FSTLevelDBMutationQueue mutationQueueWithUser:user db:_ptr serializer:self.serializer];
+}
+
+- (id<FSTQueryCache>)queryCache {
+ return [[FSTLevelDBQueryCache alloc] initWithDB:_ptr serializer:self.serializer];
+}
+
+- (id<FSTRemoteDocumentCache>)remoteDocumentCache {
+ return [[FSTLevelDBRemoteDocumentCache alloc] initWithDB:_ptr serializer:self.serializer];
+}
+
+- (FSTWriteGroup *)startGroupWithAction:(NSString *)action {
+ return [self.writeGroupTracker startGroupWithAction:action];
+}
+
+- (void)commitGroup:(FSTWriteGroup *)group {
+ [self.writeGroupTracker endGroup:group];
+
+ NSString *description = [group description];
+ FSTLog(@"Committing %@", description);
+
+ Status status = [group writeToDB:_ptr];
+ if (!status.ok()) {
+ FSTFail(@"%@ failed with status: %s, description: %@", group.action, status.ToString().c_str(),
+ description);
+ }
+}
+
+- (void)shutdown {
+ FSTAssert(self.isStarted, @"FSTLevelDB shutdown without start!");
+ self.started = NO;
+ _ptr.reset();
+}
+
+#pragma mark - Error and Status
+
++ (nullable NSError *)errorWithStatus:(Status)status description:(NSString *)description, ... {
+ if (status.ok()) {
+ return nil;
+ }
+
+ va_list args;
+ va_start(args, description);
+
+ NSString *message = [[NSString alloc] initWithFormat:description arguments:args];
+ NSString *reason = [self descriptionOfStatus:status];
+ NSError *result = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeInternal
+ userInfo:@{
+ NSLocalizedDescriptionKey : message,
+ NSLocalizedFailureReasonErrorKey : reason
+ }];
+
+ va_end(args);
+
+ return result;
+}
+
++ (NSString *)descriptionOfStatus:(Status)status {
+ return [NSString stringWithCString:status.ToString().c_str() encoding:NSUTF8StringEncoding];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDBKey.h b/Firestore/Source/Local/FSTLevelDBKey.h
new file mode 100644
index 0000000..bad7829
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDBKey.h
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 __cplusplus
+#error "FSTLevelDBKey is Objective-C++ and can only be included from .mm files"
+#endif
+
+#import <Foundation/Foundation.h>
+
+#import "FSTTypes.h"
+
+#import "StringView.h"
+
+@class FSTDocumentKey;
+@class FSTResourcePath;
+
+NS_ASSUME_NONNULL_BEGIN
+
+// All leveldb logical tables should have their keys structures described in this file.
+//
+// mutations:
+// - tableName: string = "mutation"
+// - userID: string
+// - batchID: FSTBatchID
+//
+// document_mutations:
+// - tableName: string = "document_mutation"
+// - userID: string
+// - path: FSTResourcePath
+// - batchID: FSTBatchID
+//
+// mutation_queues:
+// - tableName: string = "mutation_queue"
+// - userID: string
+//
+// targets:
+// - tableName: string = "target"
+// - targetId: FSTTargetID
+//
+// target_globals:
+// - tableName: string = "target_global"
+//
+// query_targets:
+// - tableName: string = "query_target"
+// - canonicalID: string
+// - targetId: FSTTargetID
+//
+// target_documents:
+// - tableName: string = "target_document"
+// - targetID: FSTTargetID
+// - path: FSTResourcePath
+//
+// document_targets:
+// - tableName: string = "document_target"
+// - path: FSTResourcePath
+// - targetID: FSTTargetID
+//
+// remote_documents:
+// - tableName: string = "remote_document"
+// - path: FSTResourcePath
+
+/** Helpers for any LevelDB key. */
+@interface FSTLevelDBKey : NSObject
+
+/**
+ * Parses the given key and returns a human readable description of its contents, suitable for
+ * error messages and logging.
+ */
++ (NSString *)descriptionForKey:(Firestore::StringView)key;
+
+@end
+
+/** A key in the mutations table. */
+@interface FSTLevelDBMutationKey : NSObject
+
+/** Creates a key prefix that points just before the first key in the table. */
++ (std::string)keyPrefix;
+
+/** Creates a key prefix that points just before the first key for the given userID. */
++ (std::string)keyPrefixWithUserID:(Firestore::StringView)userID;
+
+/** Creates a complete key that points to a specific userID and batchID. */
++ (std::string)keyWithUserID:(Firestore::StringView)userID batchID:(FSTBatchID)batchID;
+
+/**
+ * Decodes the given complete key, storing the decoded values as properties of the receiver.
+ *
+ * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of
+ * the receiver are in an undefined state until the next call to -decodeKey:.
+ */
+- (BOOL)decodeKey:(Firestore::StringView)key;
+
+/** The user that owns the mutation batches. */
+@property(nonatomic, assign, readonly) const std::string &userID;
+
+/** The batchID of the batch. */
+@property(nonatomic, assign, readonly) FSTBatchID batchID;
+
+@end
+
+/**
+ * A key in the document mutations index, which stores the batches in which documents are mutated.
+ */
+@interface FSTLevelDBDocumentMutationKey : NSObject
+
+/** Creates a key prefix that points just before the first key in the table. */
++ (std::string)keyPrefix;
+
+/** Creates a key prefix that points just before the first key for the given userID. */
++ (std::string)keyPrefixWithUserID:(Firestore::StringView)userID;
+
+/**
+ * Creates a key prefix that points just before the first key for the userID and resource path.
+ *
+ * Note that this uses an FSTResourcePath rather than an FSTDocumentKey in order to allow prefix
+ * scans over a collection. However a naive scan over those results isn't useful since it would
+ * match both immediate children of the collection and any subcollections.
+ */
++ (std::string)keyPrefixWithUserID:(Firestore::StringView)userID
+ resourcePath:(FSTResourcePath *)resourcePath;
+
+/** Creates a complete key that points to a specific userID, document key, and batchID. */
++ (std::string)keyWithUserID:(Firestore::StringView)userID
+ documentKey:(FSTDocumentKey *)documentKey
+ batchID:(FSTBatchID)batchID;
+
+/**
+ * Decodes the given complete key, storing the decoded values as properties of the receiver.
+ *
+ * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of
+ * the receiver are in an undefined state until the next call to -decodeKey:.
+ */
+- (BOOL)decodeKey:(Firestore::StringView)key;
+
+/** The user that owns the mutation batches. */
+@property(nonatomic, assign, readonly) const std::string &userID;
+
+/** The path to the document, as encoded in the key. */
+@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey;
+
+/** The batchID in which the document participates. */
+@property(nonatomic, assign, readonly) FSTBatchID batchID;
+
+@end
+
+/**
+ * A key in the mutation_queues table.
+ *
+ * Note that where mutation_queues contains one row about each queue, mutations contains the actual
+ * mutation batches themselves.
+ */
+@interface FSTLevelDBMutationQueueKey : NSObject
+
+/** Creates a key prefix that points just before the first key in the table. */
++ (std::string)keyPrefix;
+
+/** Creates a complete key that points to a specific mutation queue entry for the given userID. */
++ (std::string)keyWithUserID:(Firestore::StringView)userID;
+
+/**
+ * Decodes the given complete key, storing the decoded values as properties of the receiver.
+ *
+ * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of
+ * the receiver are in an undefined state until the next call to -decodeKey:.
+ */
+- (BOOL)decodeKey:(Firestore::StringView)key;
+
+@property(nonatomic, assign, readonly) const std::string &userID;
+
+@end
+
+/** A key in the target globals table, a record of global values across all targets. */
+@interface FSTLevelDBTargetGlobalKey : NSObject
+
+/** Creates a key that points to the single target global row. */
++ (std::string)key;
+
+/**
+ * Decodes the contents of a target global key, essentially just verifying that the key has the
+ * correct table name.
+ */
+- (BOOL)decodeKey:(Firestore::StringView)key;
+
+@end
+
+/** A key in the targets table. */
+@interface FSTLevelDBTargetKey : NSObject
+
+/** Creates a key prefix that points just before the first key in the table. */
++ (std::string)keyPrefix;
+
+/** Creates a complete key that points to a specific target, by targetID. */
++ (std::string)keyWithTargetID:(FSTTargetID)targetID;
+
+/**
+ * Decodes the contents of a target key into properties on this instance.
+ *
+ * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of
+ * the receiver are in an undefined state until the next call to -decodeKey:.
+ */
+- (BOOL)decodeKey:(Firestore::StringView)key;
+
+/** The targetID identifying a target. */
+@property(nonatomic, assign, readonly) FSTTargetID targetID;
+
+@end
+
+/**
+ * A key in the query targets table, an index of canonicalIDs to the targets they may match. This
+ * is not a unique mapping because canonicalID does not promise a unique name for all possible
+ * queries.
+ */
+@interface FSTLevelDBQueryTargetKey : NSObject
+
+/**
+ * Creates a key that contains just the query targets table prefix and points just before the
+ * first key.
+ */
++ (std::string)keyPrefix;
+
+/** Creates a key that points to the first query-target association for a canonicalID. */
++ (std::string)keyPrefixWithCanonicalID:(Firestore::StringView)canonicalID;
+
+/** Creates a key that points to a specific query-target entry. */
++ (std::string)keyWithCanonicalID:(Firestore::StringView)canonicalID targetID:(FSTTargetID)targetID;
+
+/** Decodes the contents of a query target key into properties on this instance. */
+- (BOOL)decodeKey:(Firestore::StringView)key;
+
+/** The canonicalID derived from the query. */
+@property(nonatomic, assign, readonly) const std::string &canonicalID;
+
+/** The targetID identifying a target. */
+@property(nonatomic, assign, readonly) FSTTargetID targetID;
+
+@end
+
+/**
+ * A key in the target documents table, an index of targetIDs to the documents they contain.
+ */
+@interface FSTLevelDBTargetDocumentKey : NSObject
+
+/**
+ * Creates a key that contains just the target documents table prefix and points just before the
+ * first key.
+ */
++ (std::string)keyPrefix;
+
+/** Creates a key that points to the first target-document association for a targetID. */
++ (std::string)keyPrefixWithTargetID:(FSTTargetID)targetID;
+
+/** Creates a key that points to a specific target-document entry. */
++ (std::string)keyWithTargetID:(FSTTargetID)targetID documentKey:(FSTDocumentKey *)documentKey;
+
+/** Decodes the contents of a target document key into properties on this instance. */
+- (BOOL)decodeKey:(Firestore::StringView)key;
+
+/** The targetID identifying a target. */
+@property(nonatomic, assign, readonly) FSTTargetID targetID;
+
+/** The path to the document, as encoded in the key. */
+@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey;
+
+@end
+
+/**
+ * A key in the document targets table, an index from documents to the targets that contain them.
+ */
+@interface FSTLevelDBDocumentTargetKey : NSObject
+
+/**
+ * Creates a key that contains just the document targets table prefix and points just before the
+ * first key.
+ */
++ (std::string)keyPrefix;
+
+/** Creates a key that points to the first document-target association for document. */
++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)resourcePath;
+
+/** Creates a key that points to a specific document-target entry. */
++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)documentKey targetID:(FSTTargetID)targetID;
+
+/** Decodes the contents of a document target key into properties on this instance. */
+- (BOOL)decodeKey:(Firestore::StringView)key;
+
+/** The targetID identifying a target. */
+@property(nonatomic, assign, readonly) FSTTargetID targetID;
+
+/** The path to the document, as encoded in the key. */
+@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey;
+
+@end
+
+/** A key in the remote documents table. */
+@interface FSTLevelDBRemoteDocumentKey : NSObject
+
+/**
+ * Creates a key that contains just the remote documents table prefix and points just before the
+ * first remote document key.
+ */
++ (std::string)keyPrefix;
+
+/**
+ * Creates a complete key that points to a specific document. The documentKey must have an even
+ * number of path segments.
+ */
++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)key;
+
+/**
+ * Creates a key prefix that contains a part of a document path. Odd numbers of segments create a
+ * collection key prefix, while an even number of segments create a document key prefix. Note that
+ * a document key prefix will match the document itself and any documents that exist in its
+ * subcollections.
+ */
++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)resourcePath;
+
+/**
+ * Decodes the contents of a remote document key into properties on this instance. This can only
+ * decode complete document paths (i.e. the result of +keyWithDocumentKey:).
+ *
+ * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of
+ * the receiver are in an undefined state until the next call to -decodeKey:.
+ */
+- (BOOL)decodeKey:(Firestore::StringView)key;
+
+/** The path to the document, as encoded in the key. */
+@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDBKey.mm b/Firestore/Source/Local/FSTLevelDBKey.mm
new file mode 100644
index 0000000..ee3e270
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDBKey.mm
@@ -0,0 +1,757 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTLevelDBKey.h"
+
+#include <string>
+
+#include "ordered_code.h"
+#include "string_util.h"
+#import "FSTDocumentKey.h"
+#import "FSTPath.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+using Firestore::OrderedCode;
+using Firestore::PrefixSuccessor;
+using Firestore::StringView;
+using leveldb::Slice;
+
+static const char *kMutationsTable = "mutation";
+static const char *kDocumentMutationsTable = "document_mutation";
+static const char *kMutationQueuesTable = "mutation_queue";
+static const char *kTargetGlobalTable = "target_global";
+static const char *kTargetsTable = "target";
+static const char *kQueryTargetsTable = "query_target";
+static const char *kTargetDocumentsTable = "target_document";
+static const char *kDocumentTargetsTable = "document_target";
+static const char *kRemoteDocumentsTable = "remote_document";
+
+/**
+ * Labels for the components of keys. These serve to make keys self-describing.
+ *
+ * These are intended to sort similarly to keys in the server storage format.
+ *
+ * Note that the server writes component labels using the equivalent to
+ * OrderedCode::WriteSignedNumDecreasing. This means that despite the higher numeric value, a
+ * terminator sorts before a path segment. In order to avoid needing the WriteSignedNumDecreasing
+ * code just for these values, this enum's values are in the reverse order to the server side.
+ *
+ * Most server-side values don't apply here. For example, the server embeds projects, databases,
+ * namespaces and similar values in its entity keys where the clients just open a different
+ * leveldb. Similarly, many of these values don't apply to the server since the server is backed
+ * by spanner which natively has concepts of tables and indexes. Where there's overlap, a comment
+ * denotes the server value from the storage_format_internal.proto.
+ */
+typedef NS_ENUM(int64_t, FSTComponentLabel) {
+ /**
+ * A terminator is the final component of a key. All complete keys have a terminator and a key
+ * is known to be a key prefix if it doesn't have a terminator.
+ */
+ FSTComponentLabelTerminator = 0, // TERMINATOR_COMPONENT = 63, server-side
+
+ /** A table name component names the logical table to which the key belongs. */
+ FSTComponentLabelTableName = 5,
+
+ /** A component containing the batch ID of a mutation. */
+ FSTComponentLabelBatchID = 10,
+
+ /** A component containing the canonical ID of a query. */
+ FSTComponentLabelCanonicalID = 11,
+
+ /** A component containing the target ID of a query. */
+ FSTComponentLabelTargetID = 12,
+
+ /** A component containing a user ID. */
+ FSTComponentLabelUserID = 13,
+
+ /**
+ * A path segment describes just a single segment in a resource path. Path segments that occur
+ * sequentially in a key represent successive segments in a single path.
+ *
+ * This value must be greater than FSTComponentLabelTerminator to ensure that longer paths sort
+ * after paths that are prefixes of them.
+ *
+ * This value must also be larger than other separators so that path suffixes sort after other
+ * key components.
+ */
+ FSTComponentLabelPathSegment = 62, // PATH = 60, server-side
+
+ /** The maximum value that can be encoded by WriteSignedNumIncreasing in a single byte. */
+ FSTComponentLabelUnknown = 63,
+};
+
+namespace {
+
+/** Writes a component label to the given key destination. */
+void WriteComponentLabel(std::string *dest, FSTComponentLabel label) {
+ OrderedCode::WriteSignedNumIncreasing(dest, label);
+}
+
+/**
+ * Reads a component label from the given key contents.
+ *
+ * If the read is unsuccessful, returns NO, and changes none of its arguments.
+ *
+ * If the read is successful, returns YES, contents will be updated to the next unread byte, and
+ * label will be set to the decoded label value.
+ */
+BOOL ReadComponentLabel(leveldb::Slice *contents, FSTComponentLabel *label) {
+ int64_t rawResult = 0;
+ Slice tmp = *contents;
+ if (OrderedCode::ReadSignedNumIncreasing(&tmp, &rawResult)) {
+ if (rawResult >= FSTComponentLabelTerminator && rawResult <= FSTComponentLabelUnknown) {
+ *label = static_cast<FSTComponentLabel>(rawResult);
+ *contents = tmp;
+ return YES;
+ }
+ }
+ return NO;
+}
+
+/**
+ * Reads a component label from the given key contents.
+ *
+ * If the read is unsuccessful or if the read was successful but the label that was read did not
+ * match the expectedLabel returns NO and changes none of its arguments.
+ *
+ * If the read is successful, returns YES and contents will be updated to the next unread byte.
+ */
+BOOL ReadComponentLabelMatching(Slice *contents, FSTComponentLabel expectedLabel) {
+ int64_t rawResult = 0;
+ Slice tmp = *contents;
+ if (OrderedCode::ReadSignedNumIncreasing(&tmp, &rawResult)) {
+ if (rawResult == expectedLabel) {
+ *contents = tmp;
+ return YES;
+ }
+ }
+ return NO;
+}
+
+/**
+ * Reads a signed number from the given key contents and verifies that the value fits in a 32-bit
+ * integer.
+ *
+ * If the read is unsuccessful or the number that was read was out of bounds for an int32_t,
+ * returns NO, and changes none of its arguments.
+ *
+ * If the read is successful, returns YES, contents will be updated to the next unread byte, and
+ * result will be set to the decoded integer value.
+ */
+BOOL ReadInt32(Slice *contents, int32_t *result) {
+ int64_t rawResult = 0;
+ Slice tmp = *contents;
+ if (OrderedCode::ReadSignedNumIncreasing(&tmp, &rawResult)) {
+ if (rawResult >= INT32_MIN && rawResult <= INT32_MAX) {
+ *contents = tmp;
+ *result = static_cast<int32_t>(rawResult);
+ return YES;
+ }
+ }
+ return NO;
+}
+
+/** Writes a component label and a signed integer to the given key destination. */
+void WriteLabeledInt32(std::string *dest, FSTComponentLabel label, int32_t value) {
+ WriteComponentLabel(dest, label);
+ OrderedCode::WriteSignedNumIncreasing(dest, value);
+}
+
+/**
+ * Reads a component label and signed number from the given key contents and verifies that the
+ * label matches the expectedLabel and the value fits in a 32-bit integer.
+ *
+ * If the read is unsuccessful, the label didn't match, or the number that was read was out of
+ * bounds for an int32_t, returns NO, and changes none of its arguments.
+ *
+ * If the read is successful, returns YES, contents will be updated to the next unread byte, and
+ * value will be set to the decoded integer value.
+ */
+BOOL ReadLabeledInt32(Slice *contents, FSTComponentLabel expectedLabel, int32_t *value) {
+ Slice tmp = *contents;
+ if (ReadComponentLabelMatching(&tmp, expectedLabel)) {
+ if (ReadInt32(&tmp, value)) {
+ *contents = tmp;
+ return YES;
+ }
+ }
+ return NO;
+}
+
+/** Writes a component label and an encoded string to the given key destination. */
+void WriteLabeledString(std::string *dest, FSTComponentLabel label, StringView value) {
+ WriteComponentLabel(dest, label);
+ OrderedCode::WriteString(dest, value);
+}
+
+/**
+ * Reads a component label and a string from the given key contents and verifies that the label
+ * matches the expectedLabel.
+ *
+ * If the read is unsuccessful or the label didn't match, returns NO, and changes none of its
+ * arguments.
+ *
+ * If the read is successful, returns YES, contents will be updated to the next unread byte, and
+ * value will be set to the decoded string value.
+ */
+BOOL ReadLabeledString(Slice *contents, FSTComponentLabel expectedLabel, std::string *value) {
+ Slice tmp = *contents;
+ if (ReadComponentLabelMatching(&tmp, expectedLabel)) {
+ if (OrderedCode::ReadString(&tmp, value)) {
+ *contents = tmp;
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
+/**
+ * Reads a component label and a string from the given key contents and verifies that the label
+ * matches the expectedLabel and the string matches the expectedValue.
+ *
+ * If the read is unsuccessful, the label or didn't match, or the string value didn't match,
+ * returns NO, and changes none of its arguments.
+ *
+ * If the read is successful, returns YES, contents will be updated to the next unread byte.
+ */
+BOOL ReadLabeledStringMatching(Slice *contents,
+ FSTComponentLabel expectedLabel,
+ const char *expectedValue) {
+ std::string value;
+ Slice tmp = *contents;
+ if (ReadLabeledString(&tmp, expectedLabel, &value)) {
+ if (value == expectedValue) {
+ *contents = tmp;
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
+/**
+ * For each segment in the given resource path writes an FSTComponentLabelPathSegment component
+ * label and a string containing the path segment.
+ */
+void WriteResourcePath(std::string *dest, FSTResourcePath *path) {
+ for (int i = 0; i < path.length; i++) {
+ WriteComponentLabel(dest, FSTComponentLabelPathSegment);
+ OrderedCode::WriteString(dest, StringView([path segmentAtIndex:i]));
+ }
+}
+
+/**
+ * Reads component labels and strings from the given key contents until it finds a component label
+ * other that FSTComponentLabelPathSegment. All matched path segments are assembled into a resource
+ * path and wrapped in an FSTDocumentKey.
+ *
+ * If the read is unsuccessful or the document key is invalid, returns NO, and changes none of its
+ * arguments.
+ *
+ * If the read is successful, returns YES, contents will be updated to the next unread byte, and
+ * value will be set to the decoded document key.
+ */
+BOOL ReadDocumentKey(Slice *contents, FSTDocumentKey *__strong *result) {
+ Slice completeSegments = *contents;
+
+ std::string segment;
+ NSMutableArray<NSString *> *pathSegments = [NSMutableArray array];
+ for (;;) {
+ // Advance a temporary slice to avoid advancing contents into the next key component which may
+ // not be a path segment.
+ Slice readPosition = completeSegments;
+ if (!ReadComponentLabelMatching(&readPosition, FSTComponentLabelPathSegment)) {
+ break;
+ }
+ if (!OrderedCode::ReadString(&readPosition, &segment)) {
+ return NO;
+ }
+
+ NSString *pathSegment = [[NSString alloc] initWithUTF8String:segment.c_str()];
+ [pathSegments addObject:pathSegment];
+ segment.clear();
+
+ completeSegments = readPosition;
+ }
+
+ FSTResourcePath *path = [FSTResourcePath pathWithSegments:pathSegments];
+ if (path.length > 0 && [FSTDocumentKey isDocumentKey:path]) {
+ *contents = completeSegments;
+ *result = [FSTDocumentKey keyWithPath:path];
+ return YES;
+ }
+
+ return NO;
+}
+
+// Trivial shortcuts that make reading and writing components type-safe.
+
+inline void WriteTerminator(std::string *dest) {
+ OrderedCode::WriteSignedNumIncreasing(dest, FSTComponentLabelTerminator);
+}
+
+inline BOOL ReadTerminator(Slice *contents) {
+ return ReadComponentLabelMatching(contents, FSTComponentLabelTerminator);
+}
+
+inline void WriteTableName(std::string *dest, const char *tableName) {
+ WriteLabeledString(dest, FSTComponentLabelTableName, tableName);
+}
+
+inline BOOL ReadTableNameMatching(Slice *contents, const char *expectedTableName) {
+ return ReadLabeledStringMatching(contents, FSTComponentLabelTableName, expectedTableName);
+}
+
+inline void WriteBatchID(std::string *dest, FSTBatchID batchID) {
+ WriteLabeledInt32(dest, FSTComponentLabelBatchID, batchID);
+}
+
+inline BOOL ReadBatchID(Slice *contents, FSTBatchID *batchID) {
+ return ReadLabeledInt32(contents, FSTComponentLabelBatchID, batchID);
+}
+
+inline void WriteCanonicalID(std::string *dest, StringView canonicalID) {
+ WriteLabeledString(dest, FSTComponentLabelCanonicalID, canonicalID);
+}
+
+inline BOOL ReadCanonicalID(Slice *contents, std::string *canonicalID) {
+ return ReadLabeledString(contents, FSTComponentLabelCanonicalID, canonicalID);
+}
+
+inline void WriteTargetID(std::string *dest, FSTTargetID targetID) {
+ WriteLabeledInt32(dest, FSTComponentLabelTargetID, targetID);
+}
+
+inline BOOL ReadTargetID(Slice *contents, FSTTargetID *targetID) {
+ return ReadLabeledInt32(contents, FSTComponentLabelTargetID, targetID);
+}
+
+inline void WriteUserID(std::string *dest, StringView userID) {
+ WriteLabeledString(dest, FSTComponentLabelUserID, userID);
+}
+
+inline BOOL ReadUserID(Slice *contents, std::string *userID) {
+ return ReadLabeledString(contents, FSTComponentLabelUserID, userID);
+}
+
+/** Returns a base64-encoded string for an invalid key, used for debug-friendly description text. */
+NSString *InvalidKey(const Slice &key) {
+ NSData *keyData =
+ [[NSData alloc] initWithBytesNoCopy:(void *)key.data() length:key.size() freeWhenDone:NO];
+ return [keyData base64EncodedStringWithOptions:0];
+}
+
+} // namespace
+
+@implementation FSTLevelDBKey
+
++ (NSString *)descriptionForKey:(StringView)key {
+ Slice contents = key;
+ BOOL isTerminated = NO;
+
+ NSMutableString *description = [NSMutableString string];
+ [description appendString:@"["];
+ while (contents.size() > 0) {
+ Slice tmp = contents;
+ FSTComponentLabel label = FSTComponentLabelUnknown;
+ if (!ReadComponentLabel(&tmp, &label)) {
+ break;
+ }
+
+ if (label == FSTComponentLabelTerminator) {
+ isTerminated = YES;
+ contents = tmp;
+ break;
+ }
+
+ // Reset tmp since all the different read routines expect to see the separator first
+ tmp = contents;
+
+ if (label == FSTComponentLabelPathSegment) {
+ FSTDocumentKey *documentKey = nil;
+ if (!ReadDocumentKey(&tmp, &documentKey)) {
+ break;
+ }
+ [description appendFormat:@" key=%@", [documentKey.path description]];
+
+ } else if (label == FSTComponentLabelTableName) {
+ std::string table;
+ if (!ReadLabeledString(&tmp, FSTComponentLabelTableName, &table)) {
+ break;
+ }
+ [description appendFormat:@"%s:", table.c_str()];
+
+ } else if (label == FSTComponentLabelBatchID) {
+ FSTBatchID batchID;
+ if (!ReadBatchID(&tmp, &batchID)) {
+ break;
+ }
+ [description appendFormat:@" batchID=%d", batchID];
+
+ } else if (label == FSTComponentLabelCanonicalID) {
+ std::string canonicalID;
+ if (!ReadCanonicalID(&tmp, &canonicalID)) {
+ break;
+ }
+ [description appendFormat:@" canonicalID=%s", canonicalID.c_str()];
+
+ } else if (label == FSTComponentLabelTargetID) {
+ FSTTargetID targetID;
+ if (!ReadTargetID(&tmp, &targetID)) {
+ break;
+ }
+ [description appendFormat:@" targetID=%d", targetID];
+
+ } else if (label == FSTComponentLabelUserID) {
+ std::string userID;
+ if (!ReadUserID(&tmp, &userID)) {
+ break;
+ }
+ [description appendFormat:@" userID=%s", userID.c_str()];
+
+ } else {
+ [description appendFormat:@" unknown label=%d", (int)label];
+ break;
+ }
+
+ contents = tmp;
+ }
+
+ if (contents.size() > 0) {
+ [description appendFormat:@" invalid key=<%@>", InvalidKey(key)];
+
+ } else if (!isTerminated) {
+ [description appendFormat:@" incomplete key"];
+ }
+
+ [description appendString:@"]"];
+ return description;
+}
+
+@end
+
+@implementation FSTLevelDBMutationKey {
+ std::string _userID;
+}
+
++ (std::string)keyPrefix {
+ std::string result;
+ WriteTableName(&result, kMutationsTable);
+ return result;
+}
+
++ (std::string)keyPrefixWithUserID:(StringView)userID {
+ std::string result;
+ WriteTableName(&result, kMutationsTable);
+ WriteUserID(&result, userID);
+ return result;
+}
+
++ (std::string)keyWithUserID:(StringView)userID batchID:(FSTBatchID)batchID {
+ std::string result;
+ WriteTableName(&result, kMutationsTable);
+ WriteUserID(&result, userID);
+ WriteBatchID(&result, batchID);
+ WriteTerminator(&result);
+ return result;
+}
+
+- (const std::string &)userID {
+ return _userID;
+}
+
+- (BOOL)decodeKey:(StringView)key {
+ _userID.clear();
+
+ Slice contents = key;
+ return ReadTableNameMatching(&contents, kMutationsTable) && ReadUserID(&contents, &_userID) &&
+ ReadBatchID(&contents, &_batchID) && ReadTerminator(&contents);
+}
+
+@end
+
+@implementation FSTLevelDBDocumentMutationKey {
+ std::string _userID;
+}
+
++ (std::string)keyPrefix {
+ std::string result;
+ WriteTableName(&result, kDocumentMutationsTable);
+ return result;
+}
+
++ (std::string)keyPrefixWithUserID:(StringView)userID {
+ std::string result;
+ WriteTableName(&result, kDocumentMutationsTable);
+ WriteUserID(&result, userID);
+ return result;
+}
+
++ (std::string)keyPrefixWithUserID:(StringView)userID resourcePath:(FSTResourcePath *)resourcePath {
+ std::string result;
+ WriteTableName(&result, kDocumentMutationsTable);
+ WriteUserID(&result, userID);
+ WriteResourcePath(&result, resourcePath);
+ return result;
+}
+
++ (std::string)keyWithUserID:(StringView)userID
+ documentKey:(FSTDocumentKey *)documentKey
+ batchID:(FSTBatchID)batchID {
+ std::string result;
+ WriteTableName(&result, kDocumentMutationsTable);
+ WriteUserID(&result, userID);
+ WriteResourcePath(&result, documentKey.path);
+ WriteBatchID(&result, batchID);
+ WriteTerminator(&result);
+ return result;
+}
+
+- (const std::string &)userID {
+ return _userID;
+}
+
+- (BOOL)decodeKey:(StringView)key {
+ _userID.clear();
+ _documentKey = nil;
+
+ Slice contents = key;
+ return ReadTableNameMatching(&contents, kDocumentMutationsTable) &&
+ ReadUserID(&contents, &_userID) && ReadDocumentKey(&contents, &_documentKey) &&
+ ReadBatchID(&contents, &_batchID) && ReadTerminator(&contents);
+}
+
+@end
+
+@implementation FSTLevelDBMutationQueueKey {
+ std::string _userID;
+}
+
++ (std::string)keyPrefix {
+ std::string result;
+ WriteTableName(&result, kMutationQueuesTable);
+ return result;
+}
+
++ (std::string)keyWithUserID:(StringView)userID {
+ std::string result;
+ WriteTableName(&result, kMutationQueuesTable);
+ WriteUserID(&result, userID);
+ WriteTerminator(&result);
+ return result;
+}
+
+- (const std::string &)userID {
+ return _userID;
+}
+
+- (BOOL)decodeKey:(StringView)key {
+ _userID.clear();
+
+ Slice contents = key;
+ return ReadTableNameMatching(&contents, kMutationQueuesTable) &&
+ ReadUserID(&contents, &_userID) && ReadTerminator(&contents);
+}
+
+@end
+
+@implementation FSTLevelDBTargetGlobalKey
+
++ (std::string)key {
+ std::string result;
+ WriteTableName(&result, kTargetGlobalTable);
+ WriteTerminator(&result);
+ return result;
+}
+
+- (BOOL)decodeKey:(StringView)key {
+ Slice contents = key;
+ return ReadTableNameMatching(&contents, kTargetGlobalTable) && ReadTerminator(&contents);
+}
+
+@end
+
+@implementation FSTLevelDBTargetKey
+
++ (std::string)keyPrefix {
+ std::string result;
+ WriteTableName(&result, kTargetsTable);
+ return result;
+}
+
++ (std::string)keyWithTargetID:(FSTTargetID)targetID {
+ std::string result;
+ WriteTableName(&result, kTargetsTable);
+ WriteTargetID(&result, targetID);
+ WriteTerminator(&result);
+ return result;
+}
+
+- (BOOL)decodeKey:(StringView)key {
+ Slice contents = key;
+ return ReadTableNameMatching(&contents, kTargetsTable) && ReadTargetID(&contents, &_targetID) &&
+ ReadTerminator(&contents);
+}
+
+@end
+
+@implementation FSTLevelDBQueryTargetKey {
+ std::string _canonicalID;
+}
+
++ (std::string)keyPrefix {
+ std::string result;
+ WriteTableName(&result, kQueryTargetsTable);
+ return result;
+}
+
++ (std::string)keyPrefixWithCanonicalID:(StringView)canonicalID {
+ std::string result;
+ WriteTableName(&result, kQueryTargetsTable);
+ WriteCanonicalID(&result, canonicalID);
+ return result;
+}
+
++ (std::string)keyWithCanonicalID:(StringView)canonicalID targetID:(FSTTargetID)targetID {
+ std::string result;
+ WriteTableName(&result, kQueryTargetsTable);
+ WriteCanonicalID(&result, canonicalID);
+ WriteTargetID(&result, targetID);
+ WriteTerminator(&result);
+ return result;
+}
+
+- (const std::string &)canonicalID {
+ return _canonicalID;
+}
+
+- (BOOL)decodeKey:(StringView)key {
+ _canonicalID.clear();
+
+ Slice contents = key;
+ return ReadTableNameMatching(&contents, kQueryTargetsTable) &&
+ ReadCanonicalID(&contents, &_canonicalID) && ReadTargetID(&contents, &_targetID) &&
+ ReadTerminator(&contents);
+}
+
+@end
+
+@implementation FSTLevelDBTargetDocumentKey
+
++ (std::string)keyPrefix {
+ std::string result;
+ WriteTableName(&result, kTargetDocumentsTable);
+ return result;
+}
+
++ (std::string)keyPrefixWithTargetID:(FSTTargetID)targetID {
+ std::string result;
+ WriteTableName(&result, kTargetDocumentsTable);
+ WriteTargetID(&result, targetID);
+ return result;
+}
+
++ (std::string)keyWithTargetID:(FSTTargetID)targetID documentKey:(FSTDocumentKey *)documentKey {
+ std::string result;
+ WriteTableName(&result, kTargetDocumentsTable);
+ WriteTargetID(&result, targetID);
+ WriteResourcePath(&result, documentKey.path);
+ WriteTerminator(&result);
+ return result;
+}
+
+- (BOOL)decodeKey:(Firestore::StringView)key {
+ _documentKey = nil;
+
+ leveldb::Slice contents = key;
+ return ReadTableNameMatching(&contents, kTargetDocumentsTable) &&
+ ReadTargetID(&contents, &_targetID) && ReadDocumentKey(&contents, &_documentKey) &&
+ ReadTerminator(&contents);
+}
+
+@end
+
+@implementation FSTLevelDBDocumentTargetKey
+
++ (std::string)keyPrefix {
+ std::string result;
+ WriteTableName(&result, kDocumentTargetsTable);
+ return result;
+}
+
++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)resourcePath {
+ std::string result;
+ WriteTableName(&result, kDocumentTargetsTable);
+ WriteResourcePath(&result, resourcePath);
+ return result;
+}
+
++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)documentKey targetID:(FSTTargetID)targetID {
+ std::string result;
+ WriteTableName(&result, kDocumentTargetsTable);
+ WriteResourcePath(&result, documentKey.path);
+ WriteTargetID(&result, targetID);
+ WriteTerminator(&result);
+ return result;
+}
+
+- (BOOL)decodeKey:(Firestore::StringView)key {
+ _documentKey = nil;
+
+ leveldb::Slice contents = key;
+ return ReadTableNameMatching(&contents, kDocumentTargetsTable) &&
+ ReadDocumentKey(&contents, &_documentKey) && ReadTargetID(&contents, &_targetID) &&
+ ReadTerminator(&contents);
+}
+
+@end
+
+@implementation FSTLevelDBRemoteDocumentKey
+
++ (std::string)keyPrefix {
+ std::string result;
+ WriteTableName(&result, kRemoteDocumentsTable);
+ return result;
+}
+
++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)path {
+ std::string result;
+ WriteTableName(&result, kRemoteDocumentsTable);
+ WriteResourcePath(&result, path);
+ return result;
+}
+
++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)key {
+ std::string result;
+ WriteTableName(&result, kRemoteDocumentsTable);
+ WriteResourcePath(&result, key.path);
+ WriteTerminator(&result);
+ return result;
+}
+
+- (BOOL)decodeKey:(StringView)key {
+ _documentKey = nil;
+
+ Slice contents = key;
+ return ReadTableNameMatching(&contents, kRemoteDocumentsTable) &&
+ ReadDocumentKey(&contents, &_documentKey) && ReadTerminator(&contents);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.h b/Firestore/Source/Local/FSTLevelDBMutationQueue.h
new file mode 100644
index 0000000..c9b5166
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FSTMutationQueue.h"
+
+#ifdef __cplusplus
+#include <memory>
+
+namespace leveldb {
+class DB;
+}
+#endif
+
+@class FSTLevelDB;
+@class FSTLocalSerializer;
+@class FSTUser;
+@protocol FSTGarbageCollector;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** A mutation queue for a specific user, backed by LevelDB. */
+@interface FSTLevelDBMutationQueue : NSObject <FSTMutationQueue>
+
+- (instancetype)init __attribute__((unavailable("Use a static constructor")));
+
+/** The garbage collector to notify about potential garbage keys. */
+@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector;
+
+#ifdef __cplusplus
+/**
+ * Creates a new mutation queue for the given user, in the given LevelDB.
+ *
+ * @param user The user for which to create a mutation queue.
+ * @param db The LevelDB in which to create the queue.
+ */
++ (instancetype)mutationQueueWithUser:(FSTUser *)user
+ db:(std::shared_ptr<leveldb::DB>)db
+ serializer:(FSTLocalSerializer *)serializer;
+
+/**
+ * Returns one larger than the largest batch ID that has been stored. If there are no mutations
+ * returns 0. Note that batch IDs are global.
+ */
++ (FSTBatchID)loadNextBatchIDFromDB:(std::shared_ptr<leveldb::DB>)db;
+#endif
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm
new file mode 100644
index 0000000..d57a15d
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm
@@ -0,0 +1,637 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTLevelDBMutationQueue.h"
+
+#include <leveldb/db.h>
+#include <leveldb/write_batch.h>
+#include <set>
+#include <string>
+
+#import "Mutation.pbobjc.h"
+#import "FSTUser.h"
+#import "FSTQuery.h"
+#import "FSTLevelDB.h"
+#import "FSTLevelDBKey.h"
+#import "FSTLocalSerializer.h"
+#import "FSTWriteGroup.h"
+#import "FSTDocumentKey.h"
+#import "FSTMutation.h"
+#import "FSTMutationBatch.h"
+#import "FSTPath.h"
+#import "FSTAssert.h"
+
+#include "ordered_code.h"
+#include "string_util.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+using Firestore::OrderedCode;
+using Firestore::StringView;
+using leveldb::DB;
+using leveldb::Iterator;
+using leveldb::ReadOptions;
+using leveldb::Slice;
+using leveldb::Status;
+using leveldb::WriteBatch;
+using leveldb::WriteOptions;
+
+@interface FSTLevelDBMutationQueue ()
+
+- (instancetype)initWithUserID:(NSString *)userID
+ db:(std::shared_ptr<DB>)db
+ serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER;
+
+/** The normalized userID (e.g. nil UID => @"" userID) used in our LevelDB keys. */
+@property(nonatomic, strong, readonly) NSString *userID;
+
+/**
+ * Next value to use when assigning sequential IDs to each mutation batch.
+ *
+ * NOTE: There can only be one FSTLevelDBMutationQueue for a given db at a time, hence it is safe
+ * to track nextBatchID as an instance-level property. Should we ever relax this constraint we'll
+ * need to revisit this.
+ */
+@property(nonatomic, assign) FSTBatchID nextBatchID;
+
+/** A write-through cache copy of the metadata describing the current queue. */
+@property(nonatomic, strong, nullable) FSTPBMutationQueue *metadata;
+
+@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer;
+
+@end
+
+/**
+ * Returns a standard set of read options.
+ *
+ * For now this is paranoid, but perhaps disable that in production builds.
+ */
+static ReadOptions StandardReadOptions() {
+ ReadOptions options;
+ options.verify_checksums = true;
+ return options;
+}
+
+@implementation FSTLevelDBMutationQueue {
+ // The DB pointer is shared with all cooperating LevelDB-related objects.
+ std::shared_ptr<DB> _db;
+}
+
++ (instancetype)mutationQueueWithUser:(FSTUser *)user
+ db:(std::shared_ptr<DB>)db
+ serializer:(FSTLocalSerializer *)serializer {
+ FSTAssert(![user.UID isEqual:@""], @"UserID must not be an empty string.");
+ NSString *userID = user.isUnauthenticated ? @"" : user.UID;
+
+ return [[FSTLevelDBMutationQueue alloc] initWithUserID:userID db:db serializer:serializer];
+}
+
+- (instancetype)initWithUserID:(NSString *)userID
+ db:(std::shared_ptr<DB>)db
+ serializer:(FSTLocalSerializer *)serializer {
+ if (self = [super init]) {
+ _userID = userID;
+ _db = db;
+ _serializer = serializer;
+ }
+ return self;
+}
+
+- (void)startWithGroup:(FSTWriteGroup *)group {
+ FSTBatchID nextBatchID = [FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db];
+
+ // On restart, nextBatchId may end up lower than lastAcknowledgedBatchId since it's computed from
+ // the queue contents, and there may be no mutations in the queue. In this case, we need to reset
+ // lastAcknowledgedBatchId (which is safe since the queue must be empty).
+ std::string key = [self keyForCurrentMutationQueue];
+ FSTPBMutationQueue *metadata = [self metadataForKey:key];
+ if (!metadata) {
+ metadata = [FSTPBMutationQueue message];
+
+ // proto3's default value for lastAcknowledgedBatchId is zero, but that would consider the first
+ // entry in the queue to be acknowledged without that acknowledgement actually happening.
+ metadata.lastAcknowledgedBatchId = kFSTBatchIDUnknown;
+ } else {
+ FSTBatchID lastAcked = metadata.lastAcknowledgedBatchId;
+ if (lastAcked >= nextBatchID) {
+ FSTAssert([self isEmpty], @"Reset nextBatchID is only possible when the queue is empty");
+ lastAcked = kFSTBatchIDUnknown;
+
+ metadata.lastAcknowledgedBatchId = lastAcked;
+ [group setMessage:metadata forKey:[self keyForCurrentMutationQueue]];
+ }
+ }
+
+ self.nextBatchID = nextBatchID;
+ self.metadata = metadata;
+}
+
+- (void)shutdown {
+ _db.reset();
+}
+
++ (FSTBatchID)loadNextBatchIDFromDB:(std::shared_ptr<DB>)db {
+ std::unique_ptr<Iterator> it(db->NewIterator(StandardReadOptions()));
+
+ auto tableKey = [FSTLevelDBMutationKey keyPrefix];
+
+ FSTLevelDBMutationKey *rowKey = [[FSTLevelDBMutationKey alloc] init];
+ FSTBatchID maxBatchID = kFSTBatchIDUnknown;
+
+ BOOL moreUserIDs = NO;
+ std::string nextUserID;
+
+ it->Seek(tableKey);
+ if (it->Valid() && [rowKey decodeKey:it->key()]) {
+ moreUserIDs = YES;
+ nextUserID = rowKey.userID;
+ }
+
+ // This loop assumes that nextUserId contains the next username at the start of the iteration.
+ while (moreUserIDs) {
+ // Compute the first key after the last mutation for nextUserID.
+ auto userEnd = [FSTLevelDBMutationKey keyPrefixWithUserID:nextUserID];
+ userEnd = Firestore::PrefixSuccessor(userEnd);
+
+ // Seek to that key with the intent of finding the boundary between nextUserID's mutations
+ // and the one after that (if any).
+ it->Seek(userEnd);
+
+ // At this point there are three possible cases to handle differently. Each case must prepare
+ // the next iteration (by assigning to nextUserID or setting moreUserIDs = NO) and seek the
+ // iterator to the last row in the current user's mutation sequence.
+ if (!it->Valid()) {
+ // The iterator is past the last row altogether (there are no additional userIDs and now
+ // rows in any table after mutations). The last row will have the highest batchID.
+ moreUserIDs = NO;
+ it->SeekToLast();
+
+ } else if ([rowKey decodeKey:it->key()]) {
+ // The iterator is valid and the key decoded successfully so the next user was just decoded.
+ nextUserID = rowKey.userID;
+ it->Prev();
+
+ } else {
+ // The iterator is past the end of the mutations table but there are other rows.
+ moreUserIDs = NO;
+ it->Prev();
+ }
+
+ // In all the cases above there was at least one row for the current user and each case has
+ // set things up such that iterator points to it.
+ if (![rowKey decodeKey:it->key()]) {
+ FSTFail(@"There should have been a key previous to %s", userEnd.c_str());
+ }
+
+ if (rowKey.batchID > maxBatchID) {
+ maxBatchID = rowKey.batchID;
+ }
+ }
+
+ return maxBatchID + 1;
+}
+
+- (BOOL)isEmpty {
+ std::string userKey = [FSTLevelDBMutationKey keyPrefixWithUserID:self.userID];
+
+ std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions()));
+ it->Seek(userKey);
+
+ BOOL empty = YES;
+ if (it->Valid() && it->key().starts_with(userKey)) {
+ empty = NO;
+ }
+
+ Status status = it->status();
+ if (!status.ok()) {
+ FSTFail(@"isEmpty failed with status: %s", status.ToString().c_str());
+ }
+
+ return empty;
+}
+
+- (FSTBatchID)highestAcknowledgedBatchID {
+ return self.metadata.lastAcknowledgedBatchId;
+}
+
+- (void)acknowledgeBatch:(FSTMutationBatch *)batch
+ streamToken:(nullable NSData *)streamToken
+ group:(FSTWriteGroup *)group {
+ FSTBatchID batchID = batch.batchID;
+ FSTAssert(batchID > self.highestAcknowledgedBatchID,
+ @"Mutation batchIDs must be acknowledged in order");
+
+ FSTPBMutationQueue *metadata = self.metadata;
+ metadata.lastAcknowledgedBatchId = batchID;
+ metadata.lastStreamToken = streamToken;
+
+ [group setMessage:metadata forKey:[self keyForCurrentMutationQueue]];
+}
+
+- (nullable NSData *)lastStreamToken {
+ return self.metadata.lastStreamToken;
+}
+
+- (void)setLastStreamToken:(nullable NSData *)streamToken group:(FSTWriteGroup *)group {
+ FSTPBMutationQueue *metadata = self.metadata;
+ metadata.lastStreamToken = streamToken;
+
+ [group setMessage:metadata forKey:[self keyForCurrentMutationQueue]];
+}
+
+- (std::string)keyForCurrentMutationQueue {
+ return [FSTLevelDBMutationQueueKey keyWithUserID:self.userID];
+}
+
+- (nullable FSTPBMutationQueue *)metadataForKey:(const std::string &)key {
+ std::string value;
+ Status status = _db->Get(StandardReadOptions(), key, &value);
+ if (status.ok()) {
+ return [self parsedMetadata:value];
+ } else if (status.IsNotFound()) {
+ return nil;
+ } else {
+ FSTFail(@"metadataForKey: failed loading key %s with status: %s", key.c_str(),
+ status.ToString().c_str());
+ }
+}
+
+- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime
+ mutations:(NSArray<FSTMutation *> *)mutations
+ group:(FSTWriteGroup *)group {
+ FSTBatchID batchID = self.nextBatchID;
+ self.nextBatchID += 1;
+
+ FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID
+ localWriteTime:localWriteTime
+ mutations:mutations];
+ std::string key = [self mutationKeyForBatch:batch];
+ [group setMessage:[self.serializer encodedMutationBatch:batch] forKey:key];
+
+ NSString *userID = self.userID;
+
+ // Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the
+ // future if we wanted to store some other kind of value here, we can parse these empty values as
+ // with some other protocol buffer (and the parser will see all default values).
+ std::string emptyBuffer;
+
+ for (FSTMutation *mutation in mutations) {
+ key = [FSTLevelDBDocumentMutationKey keyWithUserID:userID
+ documentKey:mutation.key
+ batchID:batchID];
+ [group setData:emptyBuffer forKey:key];
+ }
+
+ return batch;
+}
+
+- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID {
+ std::string key = [self mutationKeyForBatchID:batchID];
+
+ std::string value;
+ Status status = _db->Get(StandardReadOptions(), key, &value);
+ if (!status.ok()) {
+ if (status.IsNotFound()) {
+ return nil;
+ }
+ FSTFail(@"Lookup mutation batch (%@, %d) failed with status: %s", self.userID, batchID,
+ status.ToString().c_str());
+ }
+
+ return [self decodedMutationBatch:value];
+}
+
+- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID {
+ std::string key = [self mutationKeyForBatchID:batchID + 1];
+ std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions()));
+ it->Seek(key);
+
+ Status status = it->status();
+ if (!status.ok()) {
+ FSTFail(@"Seek to mutation batch (%@, %d) failed with status: %s", self.userID, batchID,
+ status.ToString().c_str());
+ }
+
+ FSTLevelDBMutationKey *rowKey = [[FSTLevelDBMutationKey alloc] init];
+ if (!it->Valid() || ![rowKey decodeKey:it->key()]) {
+ // Past the last row in the DB or out of the mutations table
+ return nil;
+ }
+
+ if (rowKey.userID != [self.userID UTF8String]) {
+ // Jumped past the last mutation for this user
+ return nil;
+ }
+
+ FSTAssert(rowKey.batchID > batchID, @"Should have found mutation after %d", batchID);
+ return [self decodedMutationBatch:it->value()];
+}
+
+- (NSArray<FSTMutationBatch *> *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID {
+ std::string userKey = [FSTLevelDBMutationKey keyPrefixWithUserID:self.userID];
+ const char *userID = [self.userID UTF8String];
+
+ std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions()));
+ it->Seek(userKey);
+
+ NSMutableArray *result = [NSMutableArray array];
+ FSTLevelDBMutationKey *rowKey = [[FSTLevelDBMutationKey alloc] init];
+ for (; it->Valid() && [rowKey decodeKey:it->key()]; it->Next()) {
+ if (rowKey.userID != userID) {
+ // End of this user's mutations
+ break;
+ } else if (rowKey.batchID > batchID) {
+ // This mutation is past what we're looking for
+ break;
+ }
+
+ [result addObject:[self decodedMutationBatch:it->value()]];
+ }
+
+ Status status = it->status();
+ if (!status.ok()) {
+ FSTFail(@"Find all mutations through mutation batch (%@, %d) failed with status: %s",
+ self.userID, batchID, status.ToString().c_str());
+ }
+
+ return result;
+}
+
+- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKey:
+ (FSTDocumentKey *)documentKey {
+ NSString *userID = self.userID;
+
+ // Scan the document-mutation index starting with a prefix starting with the given documentKey.
+ std::string indexPrefix =
+ [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID resourcePath:documentKey.path];
+ std::unique_ptr<Iterator> indexIterator(_db->NewIterator(StandardReadOptions()));
+ indexIterator->Seek(indexPrefix);
+
+ // Simultaneously scan the mutation queue. This works because each (key, batchID) pair is unique
+ // and ordered, so when scanning a table prefixed by exactly key, all the batchIDs encountered
+ // will be unique and in order.
+ std::string mutationsPrefix = [FSTLevelDBMutationKey keyPrefixWithUserID:userID];
+ std::unique_ptr<Iterator> mutationIterator(_db->NewIterator(StandardReadOptions()));
+
+ NSMutableArray *result = [NSMutableArray array];
+ FSTLevelDBDocumentMutationKey *rowKey = [[FSTLevelDBDocumentMutationKey alloc] init];
+ for (; indexIterator->Valid(); indexIterator->Next()) {
+ Slice indexKey = indexIterator->key();
+
+ // Only consider rows matching exactly the specific key of interest. Note that because we order
+ // by path first, and we order terminators before path separators, we'll encounter all the
+ // index rows for documentKey contiguously. In particular, all the rows for documentKey will
+ // occur before any rows for documents nested in a subcollection beneath documentKey so we can
+ // stop as soon as we hit any such row.
+ if (!indexKey.starts_with(indexPrefix) || ![rowKey decodeKey:indexKey] ||
+ ![rowKey.documentKey isEqualToKey:documentKey]) {
+ break;
+ }
+
+ // Each row is a unique combination of key and batchID, so this foreign key reference can
+ // only occur once.
+ std::string mutationKey = [FSTLevelDBMutationKey keyWithUserID:userID batchID:rowKey.batchID];
+ mutationIterator->Seek(mutationKey);
+ if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) {
+ NSString *foundKeyDescription = @"the end of the table";
+ if (mutationIterator->Valid()) {
+ foundKeyDescription = [FSTLevelDBKey descriptionForKey:mutationIterator->key()];
+ }
+ FSTFail(@"Dangling document-mutation reference found: "
+ @"%@ points to %@; seeking there found %@",
+ [FSTLevelDBKey descriptionForKey:indexKey],
+ [FSTLevelDBKey descriptionForKey:mutationKey], foundKeyDescription);
+ }
+
+ [result addObject:[self decodedMutationBatch:mutationIterator->value()]];
+ }
+ return result;
+}
+
+- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingQuery:(FSTQuery *)query {
+ FSTAssert(![query isDocumentQuery], @"Document queries shouldn't go down this path");
+ NSString *userID = self.userID;
+
+ FSTResourcePath *queryPath = query.path;
+ int immediateChildrenPathLength = queryPath.length + 1;
+
+ // TODO(mcg): Actually implement a single-collection query
+ //
+ // This is actually executing an ancestor query, traversing the whole subtree below the
+ // collection which can be horrifically inefficient for some structures. The right way to
+ // solve this is to implement the full value index, but that's not in the cards in the near
+ // future so this is the best we can do for the moment.
+ //
+ // Since we don't yet index the actual properties in the mutations, our current approach is to
+ // just return all mutation batches that affect documents in the collection being queried.
+ //
+ // Unlike allMutationBatchesAffectingDocumentKey, this iteration will scan the document-mutation
+ // index for more than a single document so the associated batchIDs will be neither necessarily
+ // unique nor in order. This means an efficient simultaneous scan isn't possible.
+ std::string indexPrefix =
+ [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID resourcePath:queryPath];
+ std::unique_ptr<Iterator> indexIterator(_db->NewIterator(StandardReadOptions()));
+ indexIterator->Seek(indexPrefix);
+
+ NSMutableArray *result = [NSMutableArray array];
+ FSTLevelDBDocumentMutationKey *rowKey = [[FSTLevelDBDocumentMutationKey alloc] init];
+
+ // Collect up unique batchIDs encountered during a scan of the index. Use a set<FSTBatchID> to
+ // accumulate batch IDs so they can be traversed in order in a scan of the main table.
+ //
+ // This method is faster than performing lookups of the keys with _db->Get and keeping a hash of
+ // batchIDs that have already been looked up. The performance difference is minor for small
+ // numbers of keys but > 30% faster for larger numbers of keys.
+ std::set<FSTBatchID> uniqueBatchIds;
+ for (; indexIterator->Valid(); indexIterator->Next()) {
+ Slice indexKey = indexIterator->key();
+
+ if (!indexKey.starts_with(indexPrefix) || ![rowKey decodeKey:indexKey]) {
+ break;
+ }
+
+ // Rows with document keys more than one segment longer than the query path can't be matches.
+ // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx.
+ // TODO(mcg): we'll need a different scanner when we implement ancestor queries.
+ if (rowKey.documentKey.path.length != immediateChildrenPathLength) {
+ continue;
+ }
+
+ uniqueBatchIds.insert(rowKey.batchID);
+ }
+
+ // Given an ordered set of unique batchIDs perform a skipping scan over the main table to find
+ // the mutation batches.
+ std::unique_ptr<Iterator> mutationIterator(_db->NewIterator(StandardReadOptions()));
+
+ for (FSTBatchID batchID : uniqueBatchIds) {
+ std::string mutationKey = [FSTLevelDBMutationKey keyWithUserID:userID batchID:batchID];
+ mutationIterator->Seek(mutationKey);
+ if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) {
+ NSString *foundKeyDescription = @"the end of the table";
+ if (mutationIterator->Valid()) {
+ foundKeyDescription = [FSTLevelDBKey descriptionForKey:mutationIterator->key()];
+ }
+ FSTFail(@"Dangling document-mutation reference found: "
+ @"Missing batch %@; seeking there found %@",
+ [FSTLevelDBKey descriptionForKey:mutationKey], foundKeyDescription);
+ }
+
+ [result addObject:[self decodedMutationBatch:mutationIterator->value()]];
+ }
+ return result;
+}
+
+- (NSArray<FSTMutationBatch *> *)allMutationBatches {
+ std::string userKey = [FSTLevelDBMutationKey keyPrefixWithUserID:self.userID];
+
+ std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions()));
+ it->Seek(userKey);
+
+ NSMutableArray *result = [NSMutableArray array];
+ for (; it->Valid() && it->key().starts_with(userKey); it->Next()) {
+ [result addObject:[self decodedMutationBatch:it->value()]];
+ }
+
+ Status status = it->status();
+ if (!status.ok()) {
+ FSTFail(@"Find all mutation batches failed with status: %s", status.ToString().c_str());
+ }
+
+ return result;
+}
+
+- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches group:(FSTWriteGroup *)group {
+ NSString *userID = self.userID;
+ id<FSTGarbageCollector> garbageCollector = self.garbageCollector;
+
+ std::unique_ptr<Iterator> checkIterator(_db->NewIterator(StandardReadOptions()));
+
+ for (FSTMutationBatch *batch in batches) {
+ FSTBatchID batchID = batch.batchID;
+ std::string key = [FSTLevelDBMutationKey keyWithUserID:userID batchID:batchID];
+
+ // As a sanity check, verify that the mutation batch exists before deleting it.
+ checkIterator->Seek(key);
+ FSTAssert(checkIterator->Valid(), @"Mutation batch %@ did not exist",
+ [FSTLevelDBKey descriptionForKey:key]);
+
+ FSTAssert(key == checkIterator->key(), @"Mutation batch %@ not found; found %@",
+ [FSTLevelDBKey descriptionForKey:key],
+ [FSTLevelDBKey descriptionForKey:checkIterator->key()]);
+
+ [group removeMessageForKey:key];
+
+ for (FSTMutation *mutation in batch.mutations) {
+ key = [FSTLevelDBDocumentMutationKey keyWithUserID:userID
+ documentKey:mutation.key
+ batchID:batchID];
+ [group removeMessageForKey:key];
+ [garbageCollector addPotentialGarbageKey:mutation.key];
+ }
+ }
+}
+
+- (void)performConsistencyCheck {
+ if (![self isEmpty]) {
+ return;
+ }
+
+ // Verify that there are no entries in the document-mutation index if the queue is empty.
+ std::string indexPrefix = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID];
+ std::unique_ptr<Iterator> indexIterator(_db->NewIterator(StandardReadOptions()));
+ indexIterator->Seek(indexPrefix);
+
+ NSMutableArray<NSString *> *danglingMutationReferences = [NSMutableArray array];
+
+ for (; indexIterator->Valid(); indexIterator->Next()) {
+ Slice indexKey = indexIterator->key();
+
+ // Only consider rows matching this index prefix for the current user.
+ if (!indexKey.starts_with(indexPrefix)) {
+ break;
+ }
+
+ [danglingMutationReferences addObject:[FSTLevelDBKey descriptionForKey:indexKey]];
+ }
+
+ FSTAssert(danglingMutationReferences.count == 0,
+ @"Document leak -- detected dangling mutation references when queue "
+ @"is empty. Dangling keys: %@",
+ danglingMutationReferences);
+}
+
+- (std::string)mutationKeyForBatch:(FSTMutationBatch *)batch {
+ return [FSTLevelDBMutationKey keyWithUserID:self.userID batchID:batch.batchID];
+}
+
+- (std::string)mutationKeyForBatchID:(FSTBatchID)batchID {
+ return [FSTLevelDBMutationKey keyWithUserID:self.userID batchID:batchID];
+}
+
+/** Parses the MutationQueue metadata from the given LevelDB row contents. */
+- (FSTPBMutationQueue *)parsedMetadata:(Slice)slice {
+ NSData *data =
+ [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO];
+
+ NSError *error;
+ FSTPBMutationQueue *proto = [FSTPBMutationQueue parseFromData:data error:&error];
+ if (!proto) {
+ FSTFail(@"FSTPBMutationQueue failed to parse: %@", error);
+ }
+
+ return proto;
+}
+
+- (FSTMutationBatch *)decodedMutationBatch:(Slice)slice {
+ NSData *data =
+ [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO];
+
+ NSError *error;
+ FSTPBWriteBatch *proto = [FSTPBWriteBatch parseFromData:data error:&error];
+ if (!proto) {
+ FSTFail(@"FSTPBMutationBatch failed to parse: %@", error);
+ }
+
+ return [self.serializer decodedMutationBatch:proto];
+}
+
+#pragma mark - FSTGarbageSource implementation
+
+- (BOOL)containsKey:(FSTDocumentKey *)documentKey {
+ std::string indexPrefix =
+ [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID resourcePath:documentKey.path];
+ std::unique_ptr<Iterator> indexIterator(_db->NewIterator(StandardReadOptions()));
+ indexIterator->Seek(indexPrefix);
+
+ if (indexIterator->Valid()) {
+ FSTLevelDBDocumentMutationKey *rowKey = [[FSTLevelDBDocumentMutationKey alloc] init];
+ Slice iteratorKey = indexIterator->key();
+
+ // Check both that the key prefix matches and that the decoded document key is exactly the key
+ // we're looking for.
+ if (iteratorKey.starts_with(indexPrefix) && [rowKey decodeKey:iteratorKey] &&
+ [rowKey.documentKey isEqualToKey:documentKey]) {
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDBQueryCache.h b/Firestore/Source/Local/FSTLevelDBQueryCache.h
new file mode 100644
index 0000000..3f24e6a
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDBQueryCache.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FSTQueryCache.h"
+
+#ifdef __cplusplus
+#include <memory>
+
+namespace leveldb {
+class DB;
+}
+#endif
+
+@class FSTLocalSerializer;
+@protocol FSTGarbageCollector;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Cached Queries backed by LevelDB. */
+@interface FSTLevelDBQueryCache : NSObject <FSTQueryCache>
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** The garbage collector to notify about potential garbage keys. */
+@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector;
+
+#ifdef __cplusplus
+/**
+ * Creates a new query cache in the given LevelDB.
+ *
+ * @param db The LevelDB in which to create the cache.
+ */
+- (instancetype)initWithDB:(std::shared_ptr<leveldb::DB>)db
+ serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER;
+#endif
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDBQueryCache.mm b/Firestore/Source/Local/FSTLevelDBQueryCache.mm
new file mode 100644
index 0000000..c1ba654
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDBQueryCache.mm
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTLevelDBQueryCache.h"
+
+#include <leveldb/db.h>
+#include <leveldb/write_batch.h>
+#include <string>
+
+#import "Target.pbobjc.h"
+#import "FSTQuery.h"
+#import "FSTLevelDBKey.h"
+#import "FSTLocalSerializer.h"
+#import "FSTQueryData.h"
+#import "FSTWriteGroup.h"
+#import "FSTDocumentKey.h"
+#import "FSTAssert.h"
+
+#include "ordered_code.h"
+#include "string_util.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+using Firestore::OrderedCode;
+using Firestore::StringView;
+using leveldb::DB;
+using leveldb::Iterator;
+using leveldb::ReadOptions;
+using leveldb::Slice;
+using leveldb::Status;
+using leveldb::WriteOptions;
+
+/**
+ * Returns a standard set of read options.
+ *
+ * For now this is paranoid, but perhaps disable that in production builds.
+ */
+static ReadOptions GetStandardReadOptions() {
+ ReadOptions options;
+ options.verify_checksums = true;
+ return options;
+}
+
+@interface FSTLevelDBQueryCache ()
+
+/** A write-through cached copy of the metadata for the query cache. */
+@property(nonatomic, strong, nullable) FSTPBTargetGlobal *metadata;
+
+@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer;
+
+@end
+
+@implementation FSTLevelDBQueryCache {
+ // The DB pointer is shared with all cooperating LevelDB-related objects.
+ std::shared_ptr<DB> _db;
+
+ /**
+ * The last received snapshot version. This is part of `metadata` but we store it separately to
+ * avoid extra conversion to/from GPBTimestamp.
+ */
+ FSTSnapshotVersion *_lastRemoteSnapshotVersion;
+}
+
+- (instancetype)initWithDB:(std::shared_ptr<DB>)db serializer:(FSTLocalSerializer *)serializer {
+ if (self = [super init]) {
+ FSTAssert(db, @"db must not be NULL");
+ _db = db;
+ _serializer = serializer;
+ }
+ return self;
+}
+
+- (void)start {
+ std::string key = [FSTLevelDBTargetGlobalKey key];
+ FSTPBTargetGlobal *metadata = [self metadataForKey:key];
+ if (!metadata) {
+ metadata = [FSTPBTargetGlobal message];
+ }
+ _lastRemoteSnapshotVersion = [self.serializer decodedVersion:metadata.lastRemoteSnapshotVersion];
+
+ self.metadata = metadata;
+}
+
+#pragma mark - FSTQueryCache implementation
+
+- (FSTTargetID)highestTargetID {
+ return self.metadata.highestTargetId;
+}
+
+- (FSTSnapshotVersion *)lastRemoteSnapshotVersion {
+ return _lastRemoteSnapshotVersion;
+}
+
+- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ group:(FSTWriteGroup *)group {
+ _lastRemoteSnapshotVersion = snapshotVersion;
+ self.metadata.lastRemoteSnapshotVersion = [self.serializer encodedVersion:snapshotVersion];
+ [group setMessage:self.metadata forKey:[FSTLevelDBTargetGlobalKey key]];
+}
+
+- (void)shutdown {
+ _db.reset();
+}
+
+- (void)addQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group {
+ // TODO(mcg): actually populate listen sequence number
+ FSTTargetID targetID = queryData.targetID;
+ std::string key = [FSTLevelDBTargetKey keyWithTargetID:targetID];
+ [group setMessage:[self.serializer encodedQueryData:queryData] forKey:key];
+
+ NSString *canonicalID = queryData.query.canonicalID;
+ std::string indexKey =
+ [FSTLevelDBQueryTargetKey keyWithCanonicalID:canonicalID targetID:targetID];
+ std::string emptyBuffer;
+ [group setData:emptyBuffer forKey:indexKey];
+
+ FSTPBTargetGlobal *metadata = self.metadata;
+ if (targetID > metadata.highestTargetId) {
+ metadata.highestTargetId = targetID;
+ [group setMessage:metadata forKey:[FSTLevelDBTargetGlobalKey key]];
+ }
+}
+
+- (void)removeQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group {
+ FSTTargetID targetID = queryData.targetID;
+
+ [self removeMatchingKeysForTargetID:targetID group:group];
+
+ std::string key = [FSTLevelDBTargetKey keyWithTargetID:targetID];
+ [group removeMessageForKey:key];
+
+ std::string indexKey =
+ [FSTLevelDBQueryTargetKey keyWithCanonicalID:queryData.query.canonicalID targetID:targetID];
+ [group removeMessageForKey:indexKey];
+}
+
+/**
+ * Looks up the query global metadata associated with the given key.
+ *
+ * @return the parsed protocol buffer message or nil if the row referenced by the given key does
+ * not exist.
+ */
+- (nullable FSTPBTargetGlobal *)metadataForKey:(const std::string &)key {
+ std::string value;
+ Status status = _db->Get(GetStandardReadOptions(), key, &value);
+ if (status.IsNotFound()) {
+ return nil;
+ } else if (!status.ok()) {
+ FSTFail(@"metadataForKey: failed loading key %s with status: %s", key.c_str(),
+ status.ToString().c_str());
+ }
+
+ NSData *data =
+ [[NSData alloc] initWithBytesNoCopy:(void *)value.data() length:value.size() freeWhenDone:NO];
+
+ NSError *error;
+ FSTPBTargetGlobal *proto = [FSTPBTargetGlobal parseFromData:data error:&error];
+ if (!proto) {
+ FSTFail(@"FSTPBTargetGlobal failed to parse: %@", error);
+ }
+
+ return proto;
+}
+
+/**
+ * Parses the given bytes as an FSTPBTarget protocol buffer and then converts to the equivalent
+ * query data.
+ */
+- (FSTQueryData *)decodedTargetWithSlice:(Slice)slice {
+ NSData *data =
+ [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO];
+
+ NSError *error;
+ FSTPBTarget *proto = [FSTPBTarget parseFromData:data error:&error];
+ if (!proto) {
+ FSTFail(@"FSTPBTarget failed to parse: %@", error);
+ }
+
+ return [self.serializer decodedQueryData:proto];
+}
+
+- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query {
+ // Scan the query-target index starting with a prefix starting with the given query's canonicalID.
+ // Note that this is a scan rather than a get because canonicalIDs are not required to be unique
+ // per target.
+ Slice canonicalID = StringView(query.canonicalID);
+ std::unique_ptr<Iterator> indexItererator(_db->NewIterator(GetStandardReadOptions()));
+ std::string indexPrefix = [FSTLevelDBQueryTargetKey keyPrefixWithCanonicalID:canonicalID];
+ indexItererator->Seek(indexPrefix);
+
+ // Simultaneously scan the targets table. This works because each (canonicalID, targetID) pair is
+ // unique and ordered, so when scanning a table prefixed by exactly one canonicalID, all the
+ // targetIDs will be unique and in order.
+ std::string targetPrefix = [FSTLevelDBTargetKey keyPrefix];
+ std::unique_ptr<Iterator> targetIterator(_db->NewIterator(GetStandardReadOptions()));
+
+ FSTLevelDBQueryTargetKey *rowKey = [[FSTLevelDBQueryTargetKey alloc] init];
+ for (; indexItererator->Valid(); indexItererator->Next()) {
+ Slice indexKey = indexItererator->key();
+
+ // Only consider rows matching exactly the specific canonicalID of interest.
+ if (!indexKey.starts_with(indexPrefix) || ![rowKey decodeKey:indexKey] ||
+ canonicalID != rowKey.canonicalID) {
+ // End of this canonicalID's possible targets.
+ break;
+ }
+
+ // Each row is a unique combination of canonicalID and targetID, so this foreign key reference
+ // can only occur once.
+ std::string targetKey = [FSTLevelDBTargetKey keyWithTargetID:rowKey.targetID];
+ targetIterator->Seek(targetKey);
+ if (!targetIterator->Valid() || targetIterator->key() != targetKey) {
+ NSString *foundKeyDescription = @"the end of the table";
+ if (targetIterator->Valid()) {
+ foundKeyDescription = [FSTLevelDBKey descriptionForKey:targetIterator->key()];
+ }
+ FSTFail(@"Dangling query-target reference found: "
+ @"%@ points to %@; seeking there found %@",
+ [FSTLevelDBKey descriptionForKey:indexKey],
+ [FSTLevelDBKey descriptionForKey:targetKey], foundKeyDescription);
+ }
+
+ // Finally after finding a potential match, check that the query is actually equal to the
+ // requested query.
+ FSTQueryData *target = [self decodedTargetWithSlice:targetIterator->value()];
+ if ([target.query isEqual:query]) {
+ return target;
+ }
+ }
+
+ return nil;
+}
+
+#pragma mark Matching Key tracking
+
+- (void)addMatchingKeys:(FSTDocumentKeySet *)keys
+ forTargetID:(FSTTargetID)targetID
+ group:(FSTWriteGroup *)group {
+ // Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the
+ // future if we wanted to store some other kind of value here, we can parse these empty values as
+ // with some other protocol buffer (and the parser will see all default values).
+ std::string emptyBuffer;
+
+ [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *documentKey, BOOL *stop) {
+ [group setData:emptyBuffer
+ forKey:[FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:documentKey]];
+ [group setData:emptyBuffer
+ forKey:[FSTLevelDBDocumentTargetKey keyWithDocumentKey:documentKey targetID:targetID]];
+ }];
+}
+
+- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys
+ forTargetID:(FSTTargetID)targetID
+ group:(FSTWriteGroup *)group {
+ [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ [group
+ removeMessageForKey:[FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key]];
+ [group
+ removeMessageForKey:[FSTLevelDBDocumentTargetKey keyWithDocumentKey:key targetID:targetID]];
+ [self.garbageCollector addPotentialGarbageKey:key];
+ }];
+}
+
+- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(FSTWriteGroup *)group {
+ std::string indexPrefix = [FSTLevelDBTargetDocumentKey keyPrefixWithTargetID:targetID];
+ std::unique_ptr<Iterator> indexIterator(_db->NewIterator(GetStandardReadOptions()));
+ indexIterator->Seek(indexPrefix);
+
+ FSTLevelDBTargetDocumentKey *rowKey = [[FSTLevelDBTargetDocumentKey alloc] init];
+ for (; indexIterator->Valid(); indexIterator->Next()) {
+ Slice indexKey = indexIterator->key();
+
+ // Only consider rows matching this specific targetID.
+ if (![rowKey decodeKey:indexKey] || rowKey.targetID != targetID) {
+ break;
+ }
+ FSTDocumentKey *documentKey = rowKey.documentKey;
+
+ // Delete both index rows
+ [group removeMessageForKey:indexKey];
+ [group removeMessageForKey:[FSTLevelDBDocumentTargetKey keyWithDocumentKey:documentKey
+ targetID:targetID]];
+ [self.garbageCollector addPotentialGarbageKey:documentKey];
+ }
+}
+
+- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID {
+ std::string indexPrefix = [FSTLevelDBTargetDocumentKey keyPrefixWithTargetID:targetID];
+ std::unique_ptr<Iterator> indexIterator(_db->NewIterator(GetStandardReadOptions()));
+ indexIterator->Seek(indexPrefix);
+
+ FSTDocumentKeySet *result = [FSTDocumentKeySet keySet];
+ FSTLevelDBTargetDocumentKey *rowKey = [[FSTLevelDBTargetDocumentKey alloc] init];
+ for (; indexIterator->Valid(); indexIterator->Next()) {
+ Slice indexKey = indexIterator->key();
+
+ // Only consider rows matching this specific targetID.
+ if (![rowKey decodeKey:indexKey] || rowKey.targetID != targetID) {
+ break;
+ }
+
+ result = [result setByAddingObject:rowKey.documentKey];
+ }
+
+ return result;
+}
+
+#pragma mark - FSTGarbageSource implementation
+
+- (BOOL)containsKey:(FSTDocumentKey *)key {
+ std::string indexPrefix = [FSTLevelDBDocumentTargetKey keyPrefixWithResourcePath:key.path];
+ std::unique_ptr<Iterator> indexIterator(_db->NewIterator(GetStandardReadOptions()));
+ indexIterator->Seek(indexPrefix);
+
+ if (indexIterator->Valid()) {
+ FSTLevelDBDocumentTargetKey *rowKey = [[FSTLevelDBDocumentTargetKey alloc] init];
+ if ([rowKey decodeKey:indexIterator->key()] && [rowKey.documentKey isEqualToKey:key]) {
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h
new file mode 100644
index 0000000..f327813
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.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>
+
+#import "FSTRemoteDocumentCache.h"
+
+#ifdef __cplusplus
+#include <memory>
+
+namespace leveldb {
+class DB;
+}
+#endif
+
+@class FSTLocalSerializer;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Cached Remote Documents backed by leveldb. */
+@interface FSTLevelDBRemoteDocumentCache : NSObject <FSTRemoteDocumentCache>
+
+- (instancetype)init NS_UNAVAILABLE;
+
+#ifdef __cplusplus
+/**
+ * Creates a new remote documents cache in the given leveldb.
+ *
+ * @param db The leveldb in which to create the cache.
+ */
+- (instancetype)initWithDB:(std::shared_ptr<leveldb::DB>)db
+ serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER;
+#endif
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm
new file mode 100644
index 0000000..e2424b9
--- /dev/null
+++ b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTLevelDBRemoteDocumentCache.h"
+
+#include <leveldb/db.h>
+#include <leveldb/write_batch.h>
+#include <string>
+
+#import "MaybeDocument.pbobjc.h"
+#import "FSTLevelDBKey.h"
+#import "FSTLocalSerializer.h"
+#import "FSTWriteGroup.h"
+#import "FSTDocument.h"
+#import "FSTDocumentDictionary.h"
+#import "FSTDocumentKey.h"
+#import "FSTDocumentSet.h"
+#import "FSTAssert.h"
+
+#include "ordered_code.h"
+#include "string_util.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+using Firestore::OrderedCode;
+using leveldb::DB;
+using leveldb::Iterator;
+using leveldb::ReadOptions;
+using leveldb::Slice;
+using leveldb::Status;
+using leveldb::WriteOptions;
+
+@interface FSTLevelDBRemoteDocumentCache ()
+
+@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer;
+
+@end
+
+/**
+ * Returns a standard set of read options.
+ *
+ * For now this is paranoid, but perhaps disable that in production builds.
+ */
+static ReadOptions StandardReadOptions() {
+ ReadOptions options;
+ options.verify_checksums = true;
+ return options;
+}
+
+@implementation FSTLevelDBRemoteDocumentCache {
+ // The DB pointer is shared with all cooperating LevelDB-related objects.
+ std::shared_ptr<DB> _db;
+}
+
+- (instancetype)initWithDB:(std::shared_ptr<DB>)db serializer:(FSTLocalSerializer *)serializer {
+ if (self = [super init]) {
+ _db = db;
+ _serializer = serializer;
+ }
+ return self;
+}
+
+- (void)shutdown {
+ _db.reset();
+}
+
+- (void)addEntry:(FSTMaybeDocument *)document group:(FSTWriteGroup *)group {
+ std::string key = [self remoteDocumentKey:document.key];
+ [group setMessage:[self.serializer encodedMaybeDocument:document] forKey:key];
+}
+
+- (void)removeEntryForKey:(FSTDocumentKey *)documentKey group:(FSTWriteGroup *)group {
+ std::string key = [self remoteDocumentKey:documentKey];
+ [group removeMessageForKey:key];
+}
+
+- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey {
+ std::string key = [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:documentKey];
+ std::string value;
+ Status status = _db->Get(StandardReadOptions(), key, &value);
+ if (status.IsNotFound()) {
+ return nil;
+ } else if (status.ok()) {
+ return [self decodedMaybeDocument:value withKey:documentKey];
+ } else {
+ FSTFail(@"Fetch document for key (%@) failed with status: %s", documentKey,
+ status.ToString().c_str());
+ }
+}
+
+- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query {
+ // TODO(mikelehen): PERF: At least filter to the documents that match the path of the query.
+ FSTDocumentDictionary *results = [FSTDocumentDictionary documentDictionary];
+
+ std::string startKey = [FSTLevelDBRemoteDocumentKey keyPrefix];
+ std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions()));
+ it->Seek(startKey);
+
+ FSTLevelDBRemoteDocumentKey *currentKey = [[FSTLevelDBRemoteDocumentKey alloc] init];
+ for (; it->Valid() && [currentKey decodeKey:it->key()]; it->Next()) {
+ FSTMaybeDocument *maybeDoc =
+ [self decodedMaybeDocument:it->value() withKey:currentKey.documentKey];
+ if ([maybeDoc isKindOfClass:[FSTDocument class]]) {
+ results = [results dictionaryBySettingObject:(FSTDocument *)maybeDoc forKey:maybeDoc.key];
+ }
+ }
+
+ Status status = it->status();
+ if (!status.ok()) {
+ FSTFail(@"Find documents matching query (%@) failed with status: %s", query,
+ status.ToString().c_str());
+ }
+
+ return results;
+}
+
+- (std::string)remoteDocumentKey:(FSTDocumentKey *)key {
+ return [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:key];
+}
+
+- (FSTMaybeDocument *)decodedMaybeDocument:(Slice)slice withKey:(FSTDocumentKey *)documentKey {
+ NSData *data =
+ [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO];
+
+ NSError *error;
+ FSTPBMaybeDocument *proto = [FSTPBMaybeDocument parseFromData:data error:&error];
+ if (!proto) {
+ FSTFail(@"FSTPBMaybeDocument failed to parse: %@", error);
+ }
+
+ FSTMaybeDocument *maybeDocument = [self.serializer decodedMaybeDocument:proto];
+ FSTAssert([maybeDocument.key isEqualToKey:documentKey],
+ @"Read document has key (%@) instead of expected key (%@).", maybeDocument.key,
+ documentKey);
+ return maybeDocument;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.h b/Firestore/Source/Local/FSTLocalDocumentsView.h
new file mode 100644
index 0000000..60571c2
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalDocumentsView.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FSTDocumentDictionary.h"
+#import "FSTDocumentKeySet.h"
+
+@class FSTDocumentKey;
+@class FSTMaybeDocument;
+@class FSTQuery;
+@protocol FSTMutationQueue;
+@protocol FSTRemoteDocumentCache;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A readonly view of the local state of all documents we're tracking (i.e. we have a cached
+ * version in remoteDocumentCache or local mutations for the document). The view is computed by
+ * applying the mutations in the FSTMutationQueue to the FSTRemoteDocumentCache.
+ */
+@interface FSTLocalDocumentsView : NSObject
+
++ (instancetype)viewWithRemoteDocumentCache:(id<FSTRemoteDocumentCache>)remoteDocumentCache
+ mutationQueue:(id<FSTMutationQueue>)mutationQueue;
+
+- (instancetype)init __attribute__((unavailable("Use a static constructor")));
+
+/**
+ * Get the local view of the document identified by `key`.
+ *
+ * @return Local view of the document or nil if we don't have any cached state for it.
+ */
+- (nullable FSTMaybeDocument *)documentForKey:(FSTDocumentKey *)key;
+
+/**
+ * Gets the local view of the documents identified by `keys`.
+ *
+ * If we don't have cached state for a document in `keys`, a FSTDeletedDocument will be stored
+ * for that key in the resulting set.
+ */
+- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys;
+
+/** Performs a query against the local view of all documents. */
+- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.m b/Firestore/Source/Local/FSTLocalDocumentsView.m
new file mode 100644
index 0000000..0cad593
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalDocumentsView.m
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTLocalDocumentsView.h"
+
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentDictionary.h"
+#import "FSTDocumentKey.h"
+#import "FSTMutation.h"
+#import "FSTMutationBatch.h"
+#import "FSTMutationQueue.h"
+#import "FSTQuery.h"
+#import "FSTRemoteDocumentCache.h"
+#import "FSTSnapshotVersion.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLocalDocumentsView ()
+- (instancetype)initWithRemoteDocumentCache:(id<FSTRemoteDocumentCache>)remoteDocumentCache
+ mutationQueue:(id<FSTMutationQueue>)mutationQueue
+ NS_DESIGNATED_INITIALIZER;
+@property(nonatomic, strong, readonly) id<FSTRemoteDocumentCache> remoteDocumentCache;
+@property(nonatomic, strong, readonly) id<FSTMutationQueue> mutationQueue;
+@end
+
+@implementation FSTLocalDocumentsView
+
++ (instancetype)viewWithRemoteDocumentCache:(id<FSTRemoteDocumentCache>)remoteDocumentCache
+ mutationQueue:(id<FSTMutationQueue>)mutationQueue {
+ return [[FSTLocalDocumentsView alloc] initWithRemoteDocumentCache:remoteDocumentCache
+ mutationQueue:mutationQueue];
+}
+
+- (instancetype)initWithRemoteDocumentCache:(id<FSTRemoteDocumentCache>)remoteDocumentCache
+ mutationQueue:(id<FSTMutationQueue>)mutationQueue {
+ if (self = [super init]) {
+ _remoteDocumentCache = remoteDocumentCache;
+ _mutationQueue = mutationQueue;
+ }
+ return self;
+}
+
+- (nullable FSTMaybeDocument *)documentForKey:(FSTDocumentKey *)key {
+ FSTMaybeDocument *_Nullable remoteDoc = [self.remoteDocumentCache entryForKey:key];
+ return [self localDocument:remoteDoc key:key];
+}
+
+- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys {
+ FSTMaybeDocumentDictionary *results = [FSTMaybeDocumentDictionary maybeDocumentDictionary];
+ for (FSTDocumentKey *key in keys.objectEnumerator) {
+ // TODO(mikelehen): PERF: Consider fetching all remote documents at once rather than one-by-one.
+ FSTMaybeDocument *maybeDoc = [self documentForKey:key];
+ // TODO(http://b/32275378): Don't conflate missing / deleted.
+ if (!maybeDoc) {
+ maybeDoc = [FSTDeletedDocument documentWithKey:key version:[FSTSnapshotVersion noVersion]];
+ }
+ results = [results dictionaryBySettingObject:maybeDoc forKey:key];
+ }
+ return results;
+}
+
+- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query {
+ if ([FSTDocumentKey isDocumentKey:query.path]) {
+ return [self documentsMatchingDocumentQuery:query.path];
+ } else {
+ return [self documentsMatchingCollectionQuery:query];
+ }
+}
+
+- (FSTDocumentDictionary *)documentsMatchingDocumentQuery:(FSTResourcePath *)docPath {
+ FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary];
+ // Just do a simple document lookup.
+ FSTMaybeDocument *doc = [self documentForKey:[FSTDocumentKey keyWithPath:docPath]];
+ if ([doc isKindOfClass:[FSTDocument class]]) {
+ result = [result dictionaryBySettingObject:(FSTDocument *)doc forKey:doc.key];
+ }
+ return result;
+}
+
+- (FSTDocumentDictionary *)documentsMatchingCollectionQuery:(FSTQuery *)query {
+ // Query the remote documents and overlay mutations.
+ // TODO(mikelehen): There may be significant overlap between the mutations affecting these
+ // remote documents and the allMutationBatchesAffectingQuery mutations. Consider optimizing.
+ __block FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query];
+ results = [self localDocuments:results];
+
+ // Now use the mutation queue to discover any other documents that may match the query after
+ // applying mutations.
+ FSTDocumentKeySet *matchingKeys = [FSTDocumentKeySet keySet];
+ NSArray<FSTMutationBatch *> *matchingMutationBatches =
+ [self.mutationQueue allMutationBatchesAffectingQuery:query];
+ for (FSTMutationBatch *batch in matchingMutationBatches) {
+ for (FSTMutation *mutation in batch.mutations) {
+ // TODO(mikelehen): PERF: Check if this mutation actually affects the query to reduce work.
+
+ // If the key is already in the results, we can skip it.
+ if (![results containsKey:mutation.key]) {
+ matchingKeys = [matchingKeys setByAddingObject:mutation.key];
+ }
+ }
+ }
+
+ // Now add in results for the matchingKeys.
+ for (FSTDocumentKey *key in matchingKeys.objectEnumerator) {
+ FSTMaybeDocument *doc = [self documentForKey:key];
+ if ([doc isKindOfClass:[FSTDocument class]]) {
+ results = [results dictionaryBySettingObject:(FSTDocument *)doc forKey:key];
+ }
+ }
+
+ // Finally, filter out any documents that don't actually match the query. Note that the extra
+ // reference here prevents ARC from deallocating the initial unfiltered results while we're
+ // enumerating them.
+ FSTDocumentDictionary *unfiltered = results;
+ [unfiltered
+ enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *doc, BOOL *stop) {
+ if (![query matchesDocument:doc]) {
+ results = [results dictionaryByRemovingObjectForKey:key];
+ }
+ }];
+
+ return results;
+}
+
+/**
+ * Takes a remote document and applies local mutations to generate the local view of the
+ * document.
+ *
+ * @param document The base remote document to apply mutations to.
+ * @param documentKey The key of the document (necessary when remoteDocument is nil).
+ */
+- (nullable FSTMaybeDocument *)localDocument:(nullable FSTMaybeDocument *)document
+ key:(FSTDocumentKey *)documentKey {
+ NSArray<FSTMutationBatch *> *batches =
+ [self.mutationQueue allMutationBatchesAffectingDocumentKey:documentKey];
+ for (FSTMutationBatch *batch in batches) {
+ document = [batch applyTo:document documentKey:documentKey];
+ }
+
+ return document;
+}
+
+/**
+ * Takes a set of remote documents and applies local mutations to generate the local view of
+ * the documents.
+ *
+ * @param documents The base remote documents to apply mutations to.
+ * @return The local view of the documents.
+ */
+- (FSTDocumentDictionary *)localDocuments:(FSTDocumentDictionary *)documents {
+ __block FSTDocumentDictionary *result = documents;
+ [documents enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *remoteDocument,
+ BOOL *stop) {
+ FSTMaybeDocument *mutatedDoc = [self localDocument:remoteDocument key:key];
+ if ([mutatedDoc isKindOfClass:[FSTDeletedDocument class]]) {
+ result = [documents dictionaryByRemovingObjectForKey:key];
+ } else if ([mutatedDoc isKindOfClass:[FSTDocument class]]) {
+ result = [documents dictionaryBySettingObject:(FSTDocument *)mutatedDoc forKey:key];
+ } else {
+ FSTFail(@"Unknown document: %@", mutatedDoc);
+ }
+ }];
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLocalSerializer.h b/Firestore/Source/Local/FSTLocalSerializer.h
new file mode 100644
index 0000000..6ca7f01
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalSerializer.h
@@ -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 <Foundation/Foundation.h>
+
+@class FSTMaybeDocument;
+@class FSTMutationBatch;
+@class FSTQueryData;
+@class FSTSerializerBeta;
+@class FSTSnapshotVersion;
+
+@class FSTPBMaybeDocument;
+@class FSTPBTarget;
+@class FSTPBWriteBatch;
+
+@class GPBTimestamp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Serializer for values stored in the LocalStore.
+ *
+ * Note that FSTLocalSerializer currently delegates to the serializer for the Firestore v1beta1 RPC
+ * protocol to save implementation time and code duplication. We'll need to revisit this when the
+ * RPC protocol we use diverges from local storage.
+ */
+@interface FSTLocalSerializer : NSObject
+
+- (instancetype)initWithRemoteSerializer:(FSTSerializerBeta *)remoteSerializer;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** Encodes an FSTMaybeDocument model to the equivalent protocol buffer for local storage. */
+- (FSTPBMaybeDocument *)encodedMaybeDocument:(FSTMaybeDocument *)document;
+
+/** Decodes an FSTPBMaybeDocument proto to the equivalent model. */
+- (FSTMaybeDocument *)decodedMaybeDocument:(FSTPBMaybeDocument *)proto;
+
+/** Encodes an FSTMutationBatch model for local storage in the mutation queue. */
+- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch;
+
+/** Decodes an FSTPBWriteBatch proto into a MutationBatch model. */
+- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch;
+
+/** Encodes an FSTQueryData model for local storage in the query cache. */
+- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData;
+
+/** Decodes an FSTPBTarget proto from local storage into an FSTQueryData model. */
+- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target;
+
+/** Encodes an FSTSnapshotVersion model into a GPBTimestamp proto. */
+- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version;
+
+/** Decodes a GPBTimestamp proto into a FSTSnapshotVersion model. */
+- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLocalSerializer.m b/Firestore/Source/Local/FSTLocalSerializer.m
new file mode 100644
index 0000000..58b09af
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalSerializer.m
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTLocalSerializer.h"
+
+#import "Document.pbobjc.h"
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTFieldValue.h"
+#import "FSTMutationBatch.h"
+#import "FSTQuery.h"
+#import "FSTQueryData.h"
+#import "FSTSerializerBeta.h"
+#import "MaybeDocument.pbobjc.h"
+#import "Mutation.pbobjc.h"
+#import "Target.pbobjc.h"
+
+@interface FSTLocalSerializer ()
+
+@property(nonatomic, strong, readonly) FSTSerializerBeta *remoteSerializer;
+
+@end
+
+/** Serializer for values stored in the LocalStore. */
+@implementation FSTLocalSerializer
+
+- (instancetype)initWithRemoteSerializer:(FSTSerializerBeta *)remoteSerializer {
+ self = [super init];
+ if (self) {
+ _remoteSerializer = remoteSerializer;
+ }
+ return self;
+}
+
+- (FSTPBMaybeDocument *)encodedMaybeDocument:(FSTMaybeDocument *)document {
+ FSTPBMaybeDocument *proto = [FSTPBMaybeDocument message];
+
+ if ([document isKindOfClass:[FSTDeletedDocument class]]) {
+ proto.noDocument = [self encodedDeletedDocument:(FSTDeletedDocument *)document];
+ } else if ([document isKindOfClass:[FSTDocument class]]) {
+ proto.document = [self encodedDocument:(FSTDocument *)document];
+ } else {
+ FSTFail(@"Unknown document type %@", NSStringFromClass([document class]));
+ }
+
+ return proto;
+}
+
+- (FSTMaybeDocument *)decodedMaybeDocument:(FSTPBMaybeDocument *)proto {
+ switch (proto.documentTypeOneOfCase) {
+ case FSTPBMaybeDocument_DocumentType_OneOfCase_Document:
+ return [self decodedDocument:proto.document];
+
+ case FSTPBMaybeDocument_DocumentType_OneOfCase_NoDocument:
+ return [self decodedDeletedDocument:proto.noDocument];
+
+ default:
+ FSTFail(@"Unknown MaybeDocument %@", proto);
+ }
+}
+
+/**
+ * Encodes a Document for local storage. This differs from the v1beta1 RPC serializer for
+ * Documents in that it preserves the updateTime, which is considered an output only value by the
+ * server.
+ */
+- (GCFSDocument *)encodedDocument:(FSTDocument *)document {
+ FSTSerializerBeta *remoteSerializer = self.remoteSerializer;
+
+ GCFSDocument *proto = [GCFSDocument message];
+ proto.name = [remoteSerializer encodedDocumentKey:document.key];
+ proto.fields = [remoteSerializer encodedFields:document.data];
+ proto.updateTime = [remoteSerializer encodedVersion:document.version];
+
+ return proto;
+}
+
+/** Decodes a Document proto to the equivalent model. */
+- (FSTDocument *)decodedDocument:(GCFSDocument *)document {
+ FSTSerializerBeta *remoteSerializer = self.remoteSerializer;
+
+ FSTObjectValue *data = [remoteSerializer decodedFields:document.fields];
+ FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:document.name];
+ FSTSnapshotVersion *version = [remoteSerializer decodedVersion:document.updateTime];
+ return [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO];
+}
+
+/** Encodes a NoDocument value to the equivalent proto. */
+- (FSTPBNoDocument *)encodedDeletedDocument:(FSTDeletedDocument *)document {
+ FSTSerializerBeta *remoteSerializer = self.remoteSerializer;
+
+ FSTPBNoDocument *proto = [FSTPBNoDocument message];
+ proto.name = [remoteSerializer encodedDocumentKey:document.key];
+ proto.readTime = [remoteSerializer encodedVersion:document.version];
+ return proto;
+}
+
+/** Decodes a NoDocument proto to the equivalent model. */
+- (FSTDeletedDocument *)decodedDeletedDocument:(FSTPBNoDocument *)proto {
+ FSTSerializerBeta *remoteSerializer = self.remoteSerializer;
+
+ FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:proto.name];
+ FSTSnapshotVersion *version = [remoteSerializer decodedVersion:proto.readTime];
+ return [FSTDeletedDocument documentWithKey:key version:version];
+}
+
+- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch {
+ FSTSerializerBeta *remoteSerializer = self.remoteSerializer;
+
+ FSTPBWriteBatch *proto = [FSTPBWriteBatch message];
+ proto.batchId = batch.batchID;
+ proto.localWriteTime = [remoteSerializer encodedTimestamp:batch.localWriteTime];
+
+ NSMutableArray<GCFSWrite *> *writes = proto.writesArray;
+ for (FSTMutation *mutation in batch.mutations) {
+ [writes addObject:[remoteSerializer encodedMutation:mutation]];
+ }
+ return proto;
+}
+
+- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch {
+ FSTSerializerBeta *remoteSerializer = self.remoteSerializer;
+
+ int batchID = batch.batchId;
+ NSMutableArray<FSTMutation *> *mutations = [NSMutableArray array];
+ for (GCFSWrite *write in batch.writesArray) {
+ [mutations addObject:[remoteSerializer decodedMutation:write]];
+ }
+
+ FSTTimestamp *localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime];
+
+ return [[FSTMutationBatch alloc] initWithBatchID:batchID
+ localWriteTime:localWriteTime
+ mutations:mutations];
+}
+
+- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData {
+ FSTSerializerBeta *remoteSerializer = self.remoteSerializer;
+
+ FSTAssert(queryData.purpose == FSTQueryPurposeListen,
+ @"only queries with purpose %lu may be stored, got %lu",
+ (unsigned long)FSTQueryPurposeListen, (unsigned long)queryData.purpose);
+
+ FSTPBTarget *proto = [FSTPBTarget message];
+ proto.targetId = queryData.targetID;
+ proto.snapshotVersion = [remoteSerializer encodedVersion:queryData.snapshotVersion];
+ proto.resumeToken = queryData.resumeToken;
+
+ FSTQuery *query = queryData.query;
+ if ([query isDocumentQuery]) {
+ proto.documents = [remoteSerializer encodedDocumentsTarget:query];
+ } else {
+ proto.query = [remoteSerializer encodedQueryTarget:query];
+ }
+
+ return proto;
+}
+
+- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target {
+ FSTSerializerBeta *remoteSerializer = self.remoteSerializer;
+
+ FSTTargetID targetID = target.targetId;
+ FSTSnapshotVersion *version = [remoteSerializer decodedVersion:target.snapshotVersion];
+ NSData *resumeToken = target.resumeToken;
+
+ FSTQuery *query;
+ switch (target.targetTypeOneOfCase) {
+ case FSTPBTarget_TargetType_OneOfCase_Documents:
+ query = [remoteSerializer decodedQueryFromDocumentsTarget:target.documents];
+ break;
+
+ case FSTPBTarget_TargetType_OneOfCase_Query:
+ query = [remoteSerializer decodedQueryFromQueryTarget:target.query];
+ break;
+
+ default:
+ FSTFail(@"Unknown Target.targetType %" PRId32, target.targetTypeOneOfCase);
+ }
+
+ return [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:version
+ resumeToken:resumeToken];
+}
+
+- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version {
+ return [self.remoteSerializer encodedVersion:version];
+}
+
+- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version {
+ return [self.remoteSerializer decodedVersion:version];
+}
+
+@end
diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h
new file mode 100644
index 0000000..0fdc08e
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalStore.h
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTDocumentDictionary.h"
+#import "FSTDocumentKeySet.h"
+#import "FSTDocumentVersionDictionary.h"
+#import "FSTTypes.h"
+
+@class FSTLocalViewChanges;
+@class FSTLocalWriteResult;
+@class FSTMutation;
+@class FSTMutationBatch;
+@class FSTMutationBatchResult;
+@class FSTQuery;
+@class FSTQueryData;
+@class FSTRemoteEvent;
+@class FSTUser;
+@protocol FSTPersistence;
+@protocol FSTGarbageCollector;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Local storage in the Firestore client. Coordinates persistence components like the mutation
+ * queue and remote document cache to present a latency compensated view of stored data.
+ *
+ * The LocalStore is responsible for accepting mutations from the Sync Engine. Writes from the
+ * client are put into a queue as provisional Mutations until they are processed by the RemoteStore
+ * and confirmed as having been written to the server.
+ *
+ * The local store provides the local version of documents that have been modified locally. It
+ * maintains the constraint:
+ *
+ * LocalDocument = RemoteDocument + Active(LocalMutations)
+ *
+ * (Active mutations are those that are enqueued and have not been previously acknowledged or
+ * rejected).
+ *
+ * The RemoteDocument ("ground truth") state is provided via the applyChangeBatch method. It will
+ * be some version of a server-provided document OR will be a server-provided document PLUS
+ * acknowledged mutations:
+ *
+ * RemoteDocument' = RemoteDocument + Acknowledged(LocalMutations)
+ *
+ * Note that this "dirty" version of a RemoteDocument will not be identical to a server base
+ * version, since it has LocalMutations added to it pending getting an authoritative copy from the
+ * server.
+ *
+ * Since LocalMutations can be rejected by the server, we have to be able to revert a LocalMutation
+ * that has already been applied to the LocalDocument (typically done by replaying all remaining
+ * LocalMutations to the RemoteDocument to re-apply).
+ *
+ * The LocalStore is responsible for the garbage collection of the documents it contains. For now,
+ * it every doc referenced by a view, the mutation queue, or the RemoteStore.
+ *
+ * It also maintains the persistence of mapping queries to resume tokens and target ids. It needs
+ * to know this data about queries to properly know what docs it would be allowed to garbage
+ * collect.
+ *
+ * The LocalStore must be able to efficiently execute queries against its local cache of the
+ * documents, to provide the initial set of results before any remote changes have been received.
+ */
+@interface FSTLocalStore : NSObject
+
+/** Creates a new instance of the FSTLocalStore with its required dependencies as parameters. */
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector
+ initialUser:(FSTUser *)initialUser NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** Performs any initial startup actions required by the local store. */
+- (void)start;
+
+/** Releases any open resources. */
+- (void)shutdown;
+
+/**
+ * Tells the FSTLocalStore that the currently authenticated user has changed.
+ *
+ * In response the local store switches the mutation queue to the new user and returns any
+ * resulting document changes.
+ */
+- (FSTMaybeDocumentDictionary *)userDidChange:(FSTUser *)user;
+
+/** Accepts locally generated Mutations and commits them to storage. */
+- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray<FSTMutation *> *)mutations;
+
+/** Returns the current value of a document with a given key, or nil if not found. */
+- (nullable FSTMaybeDocument *)readDocument:(FSTDocumentKey *)key;
+
+/**
+ * Acknowledges the given batch.
+ *
+ * On the happy path when a batch is acknowledged, the local store will
+ *
+ * + remove the batch from the mutation queue;
+ * + apply the changes to the remote document cache;
+ * + recalculate the latency compensated view implied by those changes (there may be mutations in
+ * the queue that affect the documents but haven't been acknowledged yet); and
+ * + give the changed documents back the sync engine
+ *
+ * @return The resulting (modified) documents.
+ */
+- (FSTMaybeDocumentDictionary *)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult;
+
+/**
+ * Removes mutations from the MutationQueue for the specified batch. LocalDocuments will be
+ * recalculated.
+ *
+ * @return The resulting (modified) documents.
+ */
+- (FSTMaybeDocumentDictionary *)rejectBatchID:(FSTBatchID)batchID;
+
+/** Returns the last recorded stream token for the current user. */
+- (nullable NSData *)lastStreamToken;
+
+/**
+ * Sets the stream token for the current user without acknowledging any mutation batch. This is
+ * usually only useful after a stream handshake or in response to an error that requires clearing
+ * the stream token.
+ */
+- (void)setLastStreamToken:(nullable NSData *)streamToken;
+
+/**
+ * Returns the last consistent snapshot processed (used by the RemoteStore to determine whether to
+ * buffer incoming snapshots from the backend).
+ */
+- (FSTSnapshotVersion *)lastRemoteSnapshotVersion;
+
+/**
+ * Updates the "ground-state" (remote) documents. We assume that the remote event reflects any
+ * write batches that have been acknowledged or rejected (i.e. we do not re-apply local mutations
+ * to updates from this event).
+ *
+ * LocalDocuments are re-calculated if there are remaining mutations in the queue.
+ */
+- (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent;
+
+/**
+ * Returns the keys of the documents that are associated with the given targetID in the remote
+ * table.
+ */
+- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID;
+
+/**
+ * Collects garbage if necessary.
+ *
+ * Should be called periodically by Sync Engine to recover resources. The implementation must
+ * guarantee that GC won't happen in other places than this method call.
+ */
+- (void)collectGarbage;
+
+/**
+ * Assigns @a query an internal ID so that its results can be pinned so they don't get GC'd.
+ * A query must be allocated in the local store before the store can be used to manage its view.
+ */
+- (FSTQueryData *)allocateQuery:(FSTQuery *)query;
+
+/** Unpin all the documents associated with @a query. */
+- (void)releaseQuery:(FSTQuery *)query;
+
+/** Runs @a query against all the documents in the local store and returns the results. */
+- (FSTDocumentDictionary *)executeQuery:(FSTQuery *)query;
+
+/** Notify the local store of the changed views to locally pin / unpin documents. */
+- (void)notifyLocalViewChanges:(NSArray<FSTLocalViewChanges *> *)viewChanges;
+
+/**
+ * Gets the mutation batch after the passed in batchId in the mutation queue or nil if empty.
+ *
+ * @param batchID The batch to search after, or -1 for the first mutation in the queue.
+ * @return the next mutation or nil if there wasn't one.
+ */
+- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLocalStore.m b/Firestore/Source/Local/FSTLocalStore.m
new file mode 100644
index 0000000..d31712a
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalStore.m
@@ -0,0 +1,546 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTLocalStore.h"
+
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentDictionary.h"
+#import "FSTDocumentKey.h"
+#import "FSTGarbageCollector.h"
+#import "FSTLocalDocumentsView.h"
+#import "FSTLocalViewChanges.h"
+#import "FSTLocalWriteResult.h"
+#import "FSTLogger.h"
+#import "FSTMutation.h"
+#import "FSTMutationBatch.h"
+#import "FSTMutationQueue.h"
+#import "FSTPersistence.h"
+#import "FSTQuery.h"
+#import "FSTQueryCache.h"
+#import "FSTQueryData.h"
+#import "FSTReferenceSet.h"
+#import "FSTRemoteDocumentCache.h"
+#import "FSTRemoteDocumentChangeBuffer.h"
+#import "FSTRemoteEvent.h"
+#import "FSTSnapshotVersion.h"
+#import "FSTTargetIDGenerator.h"
+#import "FSTTimestamp.h"
+#import "FSTUser.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLocalStore ()
+
+/** Manages our in-memory or durable persistence. */
+@property(nonatomic, strong, readonly) id<FSTPersistence> persistence;
+
+/** The set of all mutations that have been sent but not yet been applied to the backend. */
+@property(nonatomic, strong) id<FSTMutationQueue> mutationQueue;
+
+/** The set of all cached remote documents. */
+@property(nonatomic, strong) id<FSTRemoteDocumentCache> remoteDocumentCache;
+
+/** The "local" view of all documents (layering mutationQueue on top of remoteDocumentCache). */
+@property(nonatomic, strong) FSTLocalDocumentsView *localDocuments;
+
+/** The set of document references maintained by any local views. */
+@property(nonatomic, strong) FSTReferenceSet *localViewReferences;
+
+/**
+ * The garbage collector collects documents that should no longer be cached (e.g. if they are no
+ * longer retained by the above reference sets and the garbage collector is performing eager
+ * collection).
+ */
+@property(nonatomic, strong) id<FSTGarbageCollector> garbageCollector;
+
+/** Maps a query to the data about that query. */
+@property(nonatomic, strong) id<FSTQueryCache> queryCache;
+
+/** Maps a targetID to data about its query. */
+@property(nonatomic, strong) NSMutableDictionary<NSNumber *, FSTQueryData *> *targetIDs;
+
+/** Used to generate targetIDs for queries tracked locally. */
+@property(nonatomic, strong) FSTTargetIDGenerator *targetIDGenerator;
+
+/**
+ * A heldBatchResult is a mutation batch result (from a write acknowledgement) that arrived before
+ * the watch stream got notified of a snapshot that includes the write.  So we "hold" it until
+ * the watch stream catches up. It ensures that the local write remains visible (latency
+ * compensation) and doesn't temporarily appear reverted because the watch stream is slower than
+ * the write stream and so wasn't reflecting it.
+ *
+ * NOTE: Eventually we want to move this functionality into the remote store.
+ */
+@property(nonatomic, strong) NSMutableArray<FSTMutationBatchResult *> *heldBatchResults;
+
+@end
+
+@implementation FSTLocalStore
+
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector
+ initialUser:(FSTUser *)initialUser {
+ if (self = [super init]) {
+ _persistence = persistence;
+ _mutationQueue = [persistence mutationQueueForUser:initialUser];
+ _remoteDocumentCache = [persistence remoteDocumentCache];
+ _queryCache = [persistence queryCache];
+ _localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:_remoteDocumentCache
+ mutationQueue:_mutationQueue];
+ _localViewReferences = [[FSTReferenceSet alloc] init];
+
+ _garbageCollector = garbageCollector;
+ [_garbageCollector addGarbageSource:_queryCache];
+ [_garbageCollector addGarbageSource:_localViewReferences];
+ [_garbageCollector addGarbageSource:_mutationQueue];
+
+ _targetIDs = [NSMutableDictionary dictionary];
+ _heldBatchResults = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (void)start {
+ [self startMutationQueue];
+ [self startQueryCache];
+}
+
+- (void)startMutationQueue {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"];
+ [self.mutationQueue startWithGroup:group];
+
+ // If we have any leftover mutation batch results from a prior run, just drop them.
+ // TODO(http://b/33446471): We probably need to repopulate heldBatchResults or similar instead,
+ // but that is not straightforward since we're not persisting the write ack versions.
+ [self.heldBatchResults removeAllObjects];
+
+ // TODO(mikelehen): This is the only usage of getAllMutationBatchesThroughBatchId:. Consider
+ // removing it in favor of a getAcknowledgedBatches method.
+ FSTBatchID highestAck = [self.mutationQueue highestAcknowledgedBatchID];
+ if (highestAck != kFSTBatchIDUnknown) {
+ NSArray<FSTMutationBatch *> *batches =
+ [self.mutationQueue allMutationBatchesThroughBatchID:highestAck];
+ if (batches.count > 0) {
+ // NOTE: This could be more efficient if we had a removeBatchesThroughBatchID, but this set
+ // should be very small and this code should go away eventually.
+ [self.mutationQueue removeMutationBatches:batches group:group];
+ }
+ }
+ [self.persistence commitGroup:group];
+}
+
+- (void)startQueryCache {
+ [self.queryCache start];
+
+ FSTTargetID targetID = [self.queryCache highestTargetID];
+ self.targetIDGenerator = [FSTTargetIDGenerator generatorForLocalStoreStartingAfterID:targetID];
+}
+
+- (void)shutdown {
+ [self.mutationQueue shutdown];
+ [self.remoteDocumentCache shutdown];
+ [self.queryCache shutdown];
+}
+
+- (FSTMaybeDocumentDictionary *)userDidChange:(FSTUser *)user {
+ // Swap out the mutation queue, grabbing the pending mutation batches before and after.
+ NSArray<FSTMutationBatch *> *oldBatches = [self.mutationQueue allMutationBatches];
+
+ [self.mutationQueue shutdown];
+ [self.garbageCollector removeGarbageSource:self.mutationQueue];
+
+ self.mutationQueue = [self.persistence mutationQueueForUser:user];
+ [self.garbageCollector addGarbageSource:self.mutationQueue];
+
+ [self startMutationQueue];
+
+ NSArray<FSTMutationBatch *> *newBatches = [self.mutationQueue allMutationBatches];
+
+ // Recreate our LocalDocumentsView using the new MutationQueue.
+ self.localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:self.remoteDocumentCache
+ mutationQueue:self.mutationQueue];
+
+ // Union the old/new changed keys.
+ FSTDocumentKeySet *changedKeys = [FSTDocumentKeySet keySet];
+ for (NSArray<FSTMutationBatch *> *batches in @[ oldBatches, newBatches ]) {
+ for (FSTMutationBatch *batch in batches) {
+ for (FSTMutation *mutation in batch.mutations) {
+ changedKeys = [changedKeys setByAddingObject:mutation.key];
+ }
+ }
+ }
+
+ // Return the set of all (potentially) changed documents as the result of the user change.
+ return [self.localDocuments documentsForKeys:changedKeys];
+}
+
+- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray<FSTMutation *> *)mutations {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Locally write mutations"];
+ FSTTimestamp *localWriteTime = [FSTTimestamp timestamp];
+ FSTMutationBatch *batch = [self.mutationQueue addMutationBatchWithWriteTime:localWriteTime
+ mutations:mutations
+ group:group];
+ [self.persistence commitGroup:group];
+
+ FSTDocumentKeySet *keys = [batch keys];
+ FSTMaybeDocumentDictionary *changedDocuments = [self.localDocuments documentsForKeys:keys];
+ return [FSTLocalWriteResult resultForBatchID:batch.batchID changes:changedDocuments];
+}
+
+- (FSTMaybeDocumentDictionary *)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Acknowledge batch"];
+ id<FSTMutationQueue> mutationQueue = self.mutationQueue;
+
+ [mutationQueue acknowledgeBatch:batchResult.batch
+ streamToken:batchResult.streamToken
+ group:group];
+
+ FSTDocumentKeySet *affected;
+ if ([self shouldHoldBatchResultWithVersion:batchResult.commitVersion]) {
+ [self.heldBatchResults addObject:batchResult];
+ affected = [FSTDocumentKeySet keySet];
+ } else {
+ FSTRemoteDocumentChangeBuffer *remoteDocuments =
+ [FSTRemoteDocumentChangeBuffer changeBufferWithCache:self.remoteDocumentCache];
+
+ affected =
+ [self releaseBatchResults:@[ batchResult ] group:group remoteDocuments:remoteDocuments];
+
+ [remoteDocuments applyToWriteGroup:group];
+ }
+
+ [self.persistence commitGroup:group];
+ [self.mutationQueue performConsistencyCheck];
+
+ return [self.localDocuments documentsForKeys:affected];
+}
+
+- (FSTMaybeDocumentDictionary *)rejectBatchID:(FSTBatchID)batchID {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Reject batch"];
+
+ FSTMutationBatch *toReject = [self.mutationQueue lookupMutationBatch:batchID];
+ FSTAssert(toReject, @"Attempt to reject nonexistent batch!");
+
+ FSTBatchID lastAcked = [self.mutationQueue highestAcknowledgedBatchID];
+ FSTAssert(batchID > lastAcked, @"Acknowledged batches can't be rejected.");
+
+ FSTDocumentKeySet *affected = [self removeMutationBatch:toReject group:group];
+
+ [self.persistence commitGroup:group];
+ [self.mutationQueue performConsistencyCheck];
+
+ return [self.localDocuments documentsForKeys:affected];
+}
+
+- (nullable NSData *)lastStreamToken {
+ return [self.mutationQueue lastStreamToken];
+}
+
+- (void)setLastStreamToken:(nullable NSData *)streamToken {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Set stream token"];
+
+ [self.mutationQueue setLastStreamToken:streamToken group:group];
+ [self.persistence commitGroup:group];
+}
+
+- (FSTSnapshotVersion *)lastRemoteSnapshotVersion {
+ return [self.queryCache lastRemoteSnapshotVersion];
+}
+
+- (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
+ id<FSTQueryCache> queryCache = self.queryCache;
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Apply remote event"];
+ FSTRemoteDocumentChangeBuffer *remoteDocuments =
+ [FSTRemoteDocumentChangeBuffer changeBufferWithCache:self.remoteDocumentCache];
+
+ [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^(
+ NSNumber *targetIDNumber, FSTTargetChange *change, BOOL *stop) {
+ FSTTargetID targetID = targetIDNumber.intValue;
+
+ // Do not ref/unref unassigned targetIDs - it may lead to leaks.
+ FSTQueryData *queryData = self.targetIDs[targetIDNumber];
+ if (!queryData) {
+ return;
+ }
+
+ FSTTargetMapping *mapping = change.mapping;
+ if (mapping) {
+ // First make sure that all references are deleted.
+ if ([mapping isKindOfClass:[FSTResetMapping class]]) {
+ FSTResetMapping *reset = (FSTResetMapping *)mapping;
+ [queryCache removeMatchingKeysForTargetID:targetID group:group];
+ [queryCache addMatchingKeys:reset.documents forTargetID:targetID group:group];
+
+ } else if ([mapping isKindOfClass:[FSTUpdateMapping class]]) {
+ FSTUpdateMapping *update = (FSTUpdateMapping *)mapping;
+ [queryCache removeMatchingKeys:update.removedDocuments forTargetID:targetID group:group];
+ [queryCache addMatchingKeys:update.addedDocuments forTargetID:targetID group:group];
+
+ } else {
+ FSTFail(@"Unknown mapping type: %@", mapping);
+ }
+ }
+
+ // Update the resume token if the change includes one. Don't clear any preexisting value.
+ NSData *resumeToken = change.resumeToken;
+ if (resumeToken.length > 0) {
+ queryData = [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion
+ resumeToken:resumeToken];
+ self.targetIDs[targetIDNumber] = queryData;
+ [self.queryCache addQueryData:queryData group:group];
+ }
+ }];
+
+ // TODO(klimt): This could probably be an NSMutableDictionary.
+ __block FSTDocumentKeySet *changedDocKeys = [FSTDocumentKeySet keySet];
+ [remoteEvent.documentUpdates
+ enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTMaybeDocument *doc, BOOL *stop) {
+ changedDocKeys = [changedDocKeys setByAddingObject:key];
+ FSTMaybeDocument *existingDoc = [remoteDocuments entryForKey:key];
+ // Make sure we don't apply an old document version to the remote cache, though we
+ // make an exception for [SnapshotVersion noVersion] which can happen for manufactured
+ // events (e.g. in the case of a limbo document resolution failing).
+ if (!existingDoc || [doc.version isEqual:[FSTSnapshotVersion noVersion]] ||
+ [doc.version compare:existingDoc.version] != NSOrderedAscending) {
+ [remoteDocuments addEntry:doc];
+ } else {
+ FSTLog(
+ @"FSTLocalStore Ignoring outdated watch update for %@. "
+ "Current version: %@ Watch version: %@",
+ key, existingDoc.version, doc.version);
+ }
+
+ // The document might be garbage because it was unreferenced by everything.
+ // Make sure to mark it as garbage if it is...
+ [self.garbageCollector addPotentialGarbageKey:key];
+ }];
+
+ // HACK: The only reason we allow omitting snapshot version is so we can synthesize remote events
+ // when we get permission denied errors while trying to resolve the state of a locally cached
+ // document that is in limbo.
+ FSTSnapshotVersion *lastRemoteVersion = [self.queryCache lastRemoteSnapshotVersion];
+ FSTSnapshotVersion *remoteVersion = remoteEvent.snapshotVersion;
+ if (![remoteVersion isEqual:[FSTSnapshotVersion noVersion]]) {
+ FSTAssert([remoteVersion compare:lastRemoteVersion] != NSOrderedAscending,
+ @"Watch stream reverted to previous snapshot?? (%@ < %@)", remoteVersion,
+ lastRemoteVersion);
+ [self.queryCache setLastRemoteSnapshotVersion:remoteVersion group:group];
+ }
+
+ FSTDocumentKeySet *releasedWriteKeys =
+ [self releaseHeldBatchResultsWithGroup:group remoteDocuments:remoteDocuments];
+
+ [remoteDocuments applyToWriteGroup:group];
+
+ [self.persistence commitGroup:group];
+
+ // Union the two key sets.
+ __block FSTDocumentKeySet *keysToRecalc = changedDocKeys;
+ [releasedWriteKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ keysToRecalc = [keysToRecalc setByAddingObject:key];
+ }];
+
+ return [self.localDocuments documentsForKeys:keysToRecalc];
+}
+
+- (void)notifyLocalViewChanges:(NSArray<FSTLocalViewChanges *> *)viewChanges {
+ FSTReferenceSet *localViewReferences = self.localViewReferences;
+ for (FSTLocalViewChanges *view in viewChanges) {
+ FSTQueryData *queryData = [self.queryCache queryDataForQuery:view.query];
+ FSTAssert(queryData, @"Local view changes contain unallocated query.");
+ FSTTargetID targetID = queryData.targetID;
+ [localViewReferences addReferencesToKeys:view.addedKeys forID:targetID];
+ [localViewReferences removeReferencesToKeys:view.removedKeys forID:targetID];
+ }
+}
+
+- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID {
+ return [self.mutationQueue nextMutationBatchAfterBatchID:batchID];
+}
+
+- (nullable FSTMaybeDocument *)readDocument:(FSTDocumentKey *)key {
+ return [self.localDocuments documentForKey:key];
+}
+
+- (FSTQueryData *)allocateQuery:(FSTQuery *)query {
+ FSTQueryData *cached = [self.queryCache queryDataForQuery:query];
+ FSTTargetID targetID;
+ if (cached) {
+ // This query has been listened to previously, so reuse the previous targetID.
+ // TODO(mcg): freshen last accessed date?
+ targetID = cached.targetID;
+ } else {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Allocate query"];
+
+ targetID = [self.targetIDGenerator nextID];
+ cached =
+ [[FSTQueryData alloc] initWithQuery:query targetID:targetID purpose:FSTQueryPurposeListen];
+ [self.queryCache addQueryData:cached group:group];
+
+ [self.persistence commitGroup:group];
+ }
+
+ // Sanity check to ensure that even when resuming a query it's not currently active.
+ FSTBoxedTargetID *boxedTargetID = @(targetID);
+ FSTAssert(!self.targetIDs[boxedTargetID], @"Tried to allocate an already allocated query: %@",
+ query);
+ self.targetIDs[boxedTargetID] = cached;
+ return cached;
+}
+
+- (void)releaseQuery:(FSTQuery *)query {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Release query"];
+
+ FSTQueryData *queryData = [self.queryCache queryDataForQuery:query];
+ FSTAssert(queryData, @"Tried to release nonexistent query: %@", query);
+
+ [self.localViewReferences removeReferencesForID:queryData.targetID];
+ if (self.garbageCollector.isEager) {
+ [self.queryCache removeQueryData:queryData group:group];
+ }
+ [self.targetIDs removeObjectForKey:@(queryData.targetID)];
+
+ // If this was the last watch target, then we won't get any more watch snapshots, so we should
+ // release any held batch results.
+ if ([self.targetIDs count] == 0) {
+ FSTRemoteDocumentChangeBuffer *remoteDocuments =
+ [FSTRemoteDocumentChangeBuffer changeBufferWithCache:self.remoteDocumentCache];
+
+ [self releaseHeldBatchResultsWithGroup:group remoteDocuments:remoteDocuments];
+
+ [remoteDocuments applyToWriteGroup:group];
+ }
+
+ [self.persistence commitGroup:group];
+}
+
+- (FSTDocumentDictionary *)executeQuery:(FSTQuery *)query {
+ return [self.localDocuments documentsMatchingQuery:query];
+}
+
+- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID {
+ return [self.queryCache matchingKeysForTargetID:targetID];
+}
+
+- (void)collectGarbage {
+ // Call collectGarbage regardless of whether isGCEnabled so the referenceSet doesn't continue to
+ // accumulate the garbage keys.
+ NSSet<FSTDocumentKey *> *garbage = [self.garbageCollector collectGarbage];
+ if (garbage.count > 0) {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Garbage Collection"];
+ for (FSTDocumentKey *key in garbage) {
+ [self.remoteDocumentCache removeEntryForKey:key group:group];
+ }
+ [self.persistence commitGroup:group];
+ }
+}
+
+/**
+ * Releases all the held mutation batches up to the current remote version received, and
+ * applies their mutations to the docs in the remote documents cache.
+ *
+ * @return the set of keys of docs that were modified by those writes.
+ */
+- (FSTDocumentKeySet *)releaseHeldBatchResultsWithGroup:(FSTWriteGroup *)group
+ remoteDocuments:
+ (FSTRemoteDocumentChangeBuffer *)remoteDocuments {
+ NSMutableArray<FSTMutationBatchResult *> *toRelease = [NSMutableArray array];
+ for (FSTMutationBatchResult *batchResult in self.heldBatchResults) {
+ if (![self isRemoteUpToVersion:batchResult.commitVersion]) {
+ break;
+ }
+ [toRelease addObject:batchResult];
+ }
+
+ if (toRelease.count == 0) {
+ return [FSTDocumentKeySet keySet];
+ } else {
+ [self.heldBatchResults removeObjectsInRange:NSMakeRange(0, toRelease.count)];
+ return [self releaseBatchResults:toRelease group:group remoteDocuments:remoteDocuments];
+ }
+}
+
+- (BOOL)isRemoteUpToVersion:(FSTSnapshotVersion *)version {
+ // If there are no watch targets, then we won't get remote snapshots, and are always "up-to-date."
+ return [version compare:self.queryCache.lastRemoteSnapshotVersion] != NSOrderedDescending ||
+ self.targetIDs.count == 0;
+}
+
+- (BOOL)shouldHoldBatchResultWithVersion:(FSTSnapshotVersion *)version {
+ // Check if watcher isn't up to date or prior results are already held.
+ return ![self isRemoteUpToVersion:version] || self.heldBatchResults.count > 0;
+}
+
+- (FSTDocumentKeySet *)releaseBatchResults:(NSArray<FSTMutationBatchResult *> *)batchResults
+ group:(FSTWriteGroup *)group
+ remoteDocuments:(FSTRemoteDocumentChangeBuffer *)remoteDocuments {
+ NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
+ for (FSTMutationBatchResult *batchResult in batchResults) {
+ [self applyBatchResult:batchResult toRemoteDocuments:remoteDocuments];
+ [batches addObject:batchResult.batch];
+ }
+
+ return [self removeMutationBatches:batches group:group];
+}
+
+- (FSTDocumentKeySet *)removeMutationBatch:(FSTMutationBatch *)batch group:(FSTWriteGroup *)group {
+ return [self removeMutationBatches:@[ batch ] group:group];
+}
+
+/** Removes all the mutation batches named in the given array. */
+- (FSTDocumentKeySet *)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches
+ group:(FSTWriteGroup *)group {
+ // TODO(klimt): Could this be an NSMutableDictionary?
+ __block FSTDocumentKeySet *affectedDocs = [FSTDocumentKeySet keySet];
+
+ for (FSTMutationBatch *batch in batches) {
+ for (FSTMutation *mutation in batch.mutations) {
+ FSTDocumentKey *key = mutation.key;
+ affectedDocs = [affectedDocs setByAddingObject:key];
+ }
+ }
+
+ [self.mutationQueue removeMutationBatches:batches group:group];
+
+ return affectedDocs;
+}
+
+- (void)applyBatchResult:(FSTMutationBatchResult *)batchResult
+ toRemoteDocuments:(FSTRemoteDocumentChangeBuffer *)remoteDocuments {
+ FSTMutationBatch *batch = batchResult.batch;
+ FSTDocumentKeySet *docKeys = batch.keys;
+ [docKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *docKey, BOOL *stop) {
+ FSTMaybeDocument *_Nullable remoteDoc = [remoteDocuments entryForKey:docKey];
+ FSTMaybeDocument *_Nullable doc = remoteDoc;
+ FSTSnapshotVersion *ackVersion = batchResult.docVersions[docKey];
+ FSTAssert(ackVersion, @"docVersions should contain every doc in the write.");
+ if (!doc || [doc.version compare:ackVersion] == NSOrderedAscending) {
+ doc = [batch applyTo:doc documentKey:docKey mutationBatchResult:batchResult];
+ if (!doc) {
+ FSTAssert(!remoteDoc, @"Mutation batch %@ applied to document %@ resulted in nil.", batch,
+ remoteDoc);
+ } else {
+ [remoteDocuments addEntry:doc];
+ }
+ }
+ }];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLocalViewChanges.h b/Firestore/Source/Local/FSTLocalViewChanges.h
new file mode 100644
index 0000000..d44959e
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalViewChanges.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>
+
+#import "FSTDocumentKeySet.h"
+
+@class FSTDocumentKey;
+@class FSTDocumentSet;
+@class FSTMutation;
+@class FSTQuery;
+@class FSTRemoteEvent;
+@class FSTViewSnapshot;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A set of changes to what documents are currently in view and out of view for a given query.
+ * These changes are sent to the LocalStore by the View (via the SyncEngine) and are used to pin /
+ * unpin documents as appropriate.
+ */
+@interface FSTLocalViewChanges : NSObject
+
++ (instancetype)changesForQuery:(FSTQuery *)query
+ addedKeys:(FSTDocumentKeySet *)addedKeys
+ removedKeys:(FSTDocumentKeySet *)removedKeys;
+
++ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot;
+
+- (id)init NS_UNAVAILABLE;
+
+@property(nonatomic, strong, readonly) FSTQuery *query;
+@property(nonatomic, strong) FSTDocumentKeySet *addedKeys;
+@property(nonatomic, strong) FSTDocumentKeySet *removedKeys;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLocalViewChanges.m b/Firestore/Source/Local/FSTLocalViewChanges.m
new file mode 100644
index 0000000..05407b2
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalViewChanges.m
@@ -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 "FSTLocalViewChanges.h"
+
+#import "FSTDocument.h"
+#import "FSTViewSnapshot.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLocalViewChanges ()
+- (instancetype)initWithQuery:(FSTQuery *)query
+ addedKeys:(FSTDocumentKeySet *)addedKeys
+ removedKeys:(FSTDocumentKeySet *)removedKeys NS_DESIGNATED_INITIALIZER;
+@end
+
+@implementation FSTLocalViewChanges
+
++ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot {
+ FSTDocumentKeySet *addedKeys = [FSTDocumentKeySet keySet];
+ FSTDocumentKeySet *removedKeys = [FSTDocumentKeySet keySet];
+
+ for (FSTDocumentViewChange *docChange in viewSnapshot.documentChanges) {
+ switch (docChange.type) {
+ case FSTDocumentViewChangeTypeAdded:
+ addedKeys = [addedKeys setByAddingObject:docChange.document.key];
+ break;
+
+ case FSTDocumentViewChangeTypeRemoved:
+ removedKeys = [removedKeys setByAddingObject:docChange.document.key];
+ break;
+
+ default:
+ // Do nothing.
+ break;
+ }
+ }
+
+ return [self changesForQuery:viewSnapshot.query addedKeys:addedKeys removedKeys:removedKeys];
+}
+
++ (instancetype)changesForQuery:(FSTQuery *)query
+ addedKeys:(FSTDocumentKeySet *)addedKeys
+ removedKeys:(FSTDocumentKeySet *)removedKeys {
+ return
+ [[FSTLocalViewChanges alloc] initWithQuery:query addedKeys:addedKeys removedKeys:removedKeys];
+}
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ addedKeys:(FSTDocumentKeySet *)addedKeys
+ removedKeys:(FSTDocumentKeySet *)removedKeys {
+ self = [super init];
+ if (self) {
+ _query = query;
+ _addedKeys = addedKeys;
+ _removedKeys = removedKeys;
+ }
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLocalWriteResult.h b/Firestore/Source/Local/FSTLocalWriteResult.h
new file mode 100644
index 0000000..4cd7d34
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalWriteResult.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 "FSTDocumentDictionary.h"
+#import "FSTTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** The result of a write to the local store. */
+@interface FSTLocalWriteResult : NSObject
+
++ (instancetype)resultForBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes;
+
+- (id)init __attribute__((unavailable("Use resultForBatchID:changes:")));
+
+@property(nonatomic, assign, readonly) FSTBatchID batchID;
+@property(nonatomic, strong, readonly) FSTMaybeDocumentDictionary *changes;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTLocalWriteResult.m b/Firestore/Source/Local/FSTLocalWriteResult.m
new file mode 100644
index 0000000..7586686
--- /dev/null
+++ b/Firestore/Source/Local/FSTLocalWriteResult.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 "FSTLocalWriteResult.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLocalWriteResult ()
+- (instancetype)initWithBatchID:(FSTBatchID)batchID
+ changes:(FSTMaybeDocumentDictionary *)changes NS_DESIGNATED_INITIALIZER;
+@end
+
+@implementation FSTLocalWriteResult
+
++ (instancetype)resultForBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes {
+ return [[FSTLocalWriteResult alloc] initWithBatchID:batchID changes:changes];
+}
+
+- (instancetype)initWithBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes {
+ self = [super init];
+ if (self) {
+ _batchID = batchID;
+ _changes = changes;
+ }
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.h b/Firestore/Source/Local/FSTMemoryMutationQueue.h
new file mode 100644
index 0000000..6d917b7
--- /dev/null
+++ b/Firestore/Source/Local/FSTMemoryMutationQueue.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 "FSTMutationQueue.h"
+
+@protocol FSTGarbageCollector;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMemoryMutationQueue : NSObject <FSTMutationQueue>
+
++ (instancetype)mutationQueue;
+
+/** The garbage collector to notify about potential garbage keys. */
+@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.m b/Firestore/Source/Local/FSTMemoryMutationQueue.m
new file mode 100644
index 0000000..6118ad6
--- /dev/null
+++ b/Firestore/Source/Local/FSTMemoryMutationQueue.m
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTMemoryMutationQueue.h"
+
+#import "FSTAssert.h"
+#import "FSTComparison.h"
+#import "FSTDocumentKey.h"
+#import "FSTDocumentReference.h"
+#import "FSTMutation.h"
+#import "FSTMutationBatch.h"
+#import "FSTPath.h"
+#import "FSTQuery.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMemoryMutationQueue ()
+
+/**
+ * A FIFO queue of all mutations to apply to the backend. Mutations are added to the end of the
+ * queue as they're written, and removed from the front of the queue as the mutations become
+ * visible or are rejected.
+ *
+ * When successfully applied, mutations must be acknowledged by the write stream and made visible
+ * on the watch stream. It's possible for the watch stream to fall behind in which case the batches
+ * at the head of the queue will be acknowledged but held until the watch stream sees the changes.
+ *
+ * If a batch is rejected while there are held write acknowledgements at the head of the queue
+ * the rejected batch is converted to a tombstone: its mutations are removed but the batch remains
+ * in the queue. This maintains a simple consecutive ordering of batches in the queue.
+ *
+ * Once the held write acknowledgements become visible they are removed from the head of the queue
+ * along with any tombstones that follow.
+ */
+@property(nonatomic, strong, readonly) NSMutableArray<FSTMutationBatch *> *queue;
+
+/** An ordered mapping between documents and the mutation batch IDs. */
+@property(nonatomic, strong) FSTImmutableSortedSet<FSTDocumentReference *> *batchesByDocumentKey;
+
+/** The next value to use when assigning sequential IDs to each mutation batch. */
+@property(nonatomic, assign) FSTBatchID nextBatchID;
+
+/** The highest acknowledged mutation in the queue. */
+@property(nonatomic, assign) FSTBatchID highestAcknowledgedBatchID;
+
+/**
+ * The last received stream token from the server, used to acknowledge which responses the client
+ * has processed. Stream tokens are opaque checkpoint markers whose only real value is their
+ * inclusion in the next request.
+ */
+@property(nonatomic, strong, nullable) NSData *lastStreamToken;
+
+@end
+
+@implementation FSTMemoryMutationQueue
+
++ (instancetype)mutationQueue {
+ return [[FSTMemoryMutationQueue alloc] init];
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _queue = [NSMutableArray array];
+ _batchesByDocumentKey =
+ [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey];
+
+ _nextBatchID = 1;
+ _highestAcknowledgedBatchID = kFSTBatchIDUnknown;
+ }
+ return self;
+}
+
+#pragma mark - FSTMutationQueue implementation
+
+- (void)startWithGroup:(FSTWriteGroup *)group {
+ // Note: The queue may be shutdown / started multiple times, since we maintain the queue for the
+ // duration of the app session in case a user logs out / back in. To behave like the
+ // LevelDB-backed MutationQueue (and accommodate tests that expect as much), we reset nextBatchID
+ // and highestAcknowledgedBatchID if the queue is empty.
+ if (self.isEmpty) {
+ self.nextBatchID = 1;
+ self.highestAcknowledgedBatchID = kFSTBatchIDUnknown;
+ }
+ FSTAssert(self.highestAcknowledgedBatchID < self.nextBatchID,
+ @"highestAcknowledgedBatchID must be less than the nextBatchID");
+}
+
+- (void)shutdown {
+}
+
+- (BOOL)isEmpty {
+ // If the queue has any entries at all, the first entry must not be a tombstone (otherwise it
+ // would have been removed already).
+ return self.queue.count == 0;
+}
+
+- (FSTBatchID)highestAcknowledgedBatchID {
+ return _highestAcknowledgedBatchID;
+}
+
+- (void)acknowledgeBatch:(FSTMutationBatch *)batch
+ streamToken:(nullable NSData *)streamToken
+ group:(__unused FSTWriteGroup *)group {
+ NSMutableArray<FSTMutationBatch *> *queue = self.queue;
+
+ FSTBatchID batchID = batch.batchID;
+ FSTAssert(batchID > self.highestAcknowledgedBatchID,
+ @"Mutation batchIDs must be acknowledged in order");
+
+ NSInteger batchIndex = [self indexOfExistingBatchID:batchID action:@"acknowledged"];
+
+ // Verify that the batch in the queue is the one to be acknowledged.
+ FSTMutationBatch *check = queue[(NSUInteger)batchIndex];
+ FSTAssert(batchID == check.batchID, @"Queue ordering failure: expected batch %d, got batch %d",
+ batchID, check.batchID);
+ FSTAssert(![check isTombstone], @"Can't acknowledge a previously removed batch");
+
+ self.highestAcknowledgedBatchID = batchID;
+ self.lastStreamToken = streamToken;
+}
+
+- (void)setLastStreamToken:(nullable NSData *)streamToken group:(__unused FSTWriteGroup *)group {
+ self.lastStreamToken = streamToken;
+}
+
+- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime
+ mutations:(NSArray<FSTMutation *> *)mutations
+ group:(FSTWriteGroup *)group {
+ FSTAssert(mutations.count > 0, @"Mutation batches should not be empty");
+
+ FSTBatchID batchID = self.nextBatchID;
+ self.nextBatchID += 1;
+
+ NSMutableArray<FSTMutationBatch *> *queue = self.queue;
+ if (queue.count > 0) {
+ FSTMutationBatch *prior = queue[queue.count - 1];
+ FSTAssert(prior.batchID < batchID, @"Mutation batchIDs must be monotonically increasing order");
+ }
+
+ FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID
+ localWriteTime:localWriteTime
+ mutations:mutations];
+ [queue addObject:batch];
+
+ // Track references by document key.
+ FSTImmutableSortedSet<FSTDocumentReference *> *references = self.batchesByDocumentKey;
+ for (FSTMutation *mutation in batch.mutations) {
+ references = [references
+ setByAddingObject:[[FSTDocumentReference alloc] initWithKey:mutation.key ID:batchID]];
+ }
+ self.batchesByDocumentKey = references;
+
+ return batch;
+}
+
+- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID {
+ NSMutableArray<FSTMutationBatch *> *queue = self.queue;
+
+ NSInteger index = [self indexOfBatchID:batchID];
+ if (index < 0 || index >= queue.count) {
+ return nil;
+ }
+
+ FSTMutationBatch *batch = queue[(NSUInteger)index];
+ FSTAssert(batch.batchID == batchID, @"If found batch must match");
+ return [batch isTombstone] ? nil : batch;
+}
+
+- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID {
+ NSMutableArray<FSTMutationBatch *> *queue = self.queue;
+ NSUInteger count = queue.count;
+
+ // All batches with batchID <= self.highestAcknowledgedBatchID have been acknowledged so the
+ // first unacknowledged batch after batchID will have a batchID larger than both of these values.
+ batchID = MAX(batchID + 1, self.highestAcknowledgedBatchID);
+
+ // The requested batchID may still be out of range so normalize it to the start of the queue.
+ NSInteger rawIndex = [self indexOfBatchID:batchID];
+ NSUInteger index = rawIndex < 0 ? 0 : (NSUInteger)rawIndex;
+
+ // Finally return the first non-tombstone batch.
+ for (; index < count; index++) {
+ FSTMutationBatch *batch = queue[index];
+ if (![batch isTombstone]) {
+ return batch;
+ }
+ }
+
+ return nil;
+}
+
+- (NSArray<FSTMutationBatch *> *)allMutationBatches {
+ return [self allLiveMutationBatchesBeforeIndex:self.queue.count];
+}
+
+- (NSArray<FSTMutationBatch *> *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID {
+ NSMutableArray<FSTMutationBatch *> *queue = self.queue;
+ NSUInteger count = queue.count;
+
+ NSInteger endIndex = [self indexOfBatchID:batchID];
+ if (endIndex < 0) {
+ endIndex = 0;
+ } else if (endIndex >= count) {
+ endIndex = count;
+ } else {
+ // The endIndex is in the queue so increment to pull everything in the queue including it.
+ endIndex += 1;
+ }
+
+ return [self allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex];
+}
+
+- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKey:
+ (FSTDocumentKey *)documentKey {
+ FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:documentKey ID:0];
+
+ NSMutableArray<FSTMutationBatch *> *result = [NSMutableArray array];
+ FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) {
+ if (![documentKey isEqualToKey:reference.key]) {
+ *stop = YES;
+ return;
+ }
+
+ FSTMutationBatch *batch = [self lookupMutationBatch:reference.ID];
+ FSTAssert(batch, @"Batches in the index must exist in the main table");
+ [result addObject:batch];
+ };
+
+ [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block];
+ return result;
+}
+
+- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingQuery:(FSTQuery *)query {
+ // Use the query path as a prefix for testing if a document matches the query.
+ FSTResourcePath *prefix = query.path;
+ int immediateChildrenPathLength = prefix.length + 1;
+
+ // Construct a document reference for actually scanning the index. Unlike the prefix, the document
+ // key in this reference must have an even number of segments. The empty segment can be used as
+ // a suffix of the query path because it precedes all other segments in an ordered traversal.
+ FSTResourcePath *startPath = query.path;
+ if (![FSTDocumentKey isDocumentKey:startPath]) {
+ startPath = [startPath pathByAppendingSegment:@""];
+ }
+ FSTDocumentReference *start =
+ [[FSTDocumentReference alloc] initWithKey:[FSTDocumentKey keyWithPath:startPath] ID:0];
+
+ // Find unique batchIDs referenced by all documents potentially matching the query.
+ __block FSTImmutableSortedSet<NSNumber *> *uniqueBatchIDs =
+ [FSTImmutableSortedSet setWithComparator:FSTNumberComparator];
+ FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) {
+ FSTResourcePath *rowKeyPath = reference.key.path;
+ if (![prefix isPrefixOfPath:rowKeyPath]) {
+ *stop = YES;
+ return;
+ }
+
+ // Rows with document keys more than one segment longer than the query path can't be matches.
+ // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx.
+ // TODO(mcg): we'll need a different scanner when we implement ancestor queries.
+ if (rowKeyPath.length != immediateChildrenPathLength) {
+ return;
+ }
+
+ uniqueBatchIDs = [uniqueBatchIDs setByAddingObject:@(reference.ID)];
+ };
+ [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block];
+
+ // Construct an array of matching batches, sorted by batchID to ensure that multiple mutations
+ // affecting the same document key are applied in order.
+ NSMutableArray<FSTMutationBatch *> *result = [NSMutableArray array];
+ [uniqueBatchIDs enumerateObjectsUsingBlock:^(NSNumber *batchID, BOOL *stop) {
+ FSTMutationBatch *batch = [self lookupMutationBatch:[batchID intValue]];
+ if (batch) {
+ [result addObject:batch];
+ }
+ }];
+
+ return result;
+}
+
+- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches group:(FSTWriteGroup *)group {
+ NSUInteger batchCount = batches.count;
+ FSTAssert(batchCount > 0, @"Should not remove mutations when none exist.");
+
+ FSTBatchID firstBatchID = batches[0].batchID;
+
+ NSMutableArray<FSTMutationBatch *> *queue = self.queue;
+ NSUInteger queueCount = queue.count;
+
+ // Find the position of the first batch for removal. This need not be the first entry in the
+ // queue.
+ NSUInteger startIndex = [self indexOfExistingBatchID:firstBatchID action:@"removed"];
+ FSTAssert(queue[startIndex].batchID == firstBatchID, @"Removed batches must exist in the queue");
+
+ // Check that removed batches are contiguous (while excluding tombstones).
+ NSUInteger batchIndex = 1;
+ NSUInteger queueIndex = startIndex + 1;
+ while (batchIndex < batchCount && queueIndex < queueCount) {
+ FSTMutationBatch *batch = queue[queueIndex];
+ if ([batch isTombstone]) {
+ queueIndex++;
+ continue;
+ }
+
+ FSTAssert(batch.batchID == batches[batchIndex].batchID,
+ @"Removed batches must be contiguous in the queue");
+ batchIndex++;
+ queueIndex++;
+ }
+
+ // Only actually remove batches if removing at the front of the queue. Previously rejected batches
+ // may have left tombstones in the queue, so expand the removal range to include any tombstones.
+ if (startIndex == 0) {
+ for (; queueIndex < queueCount; queueIndex++) {
+ FSTMutationBatch *batch = queue[queueIndex];
+ if (![batch isTombstone]) {
+ break;
+ }
+ }
+
+ NSUInteger length = queueIndex - startIndex;
+ [queue removeObjectsInRange:NSMakeRange(startIndex, length)];
+
+ } else {
+ // Mark tombstones
+ for (NSUInteger i = startIndex; i < queueIndex; i++) {
+ queue[i] = [queue[i] toTombstone];
+ }
+ }
+
+ // Remove entries from the index too.
+ id<FSTGarbageCollector> garbageCollector = self.garbageCollector;
+ FSTImmutableSortedSet<FSTDocumentReference *> *references = self.batchesByDocumentKey;
+ for (FSTMutationBatch *batch in batches) {
+ FSTBatchID batchID = batch.batchID;
+ for (FSTMutation *mutation in batch.mutations) {
+ FSTDocumentKey *key = mutation.key;
+ [garbageCollector addPotentialGarbageKey:key];
+
+ FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:batchID];
+ references = [references setByRemovingObject:reference];
+ }
+ }
+ self.batchesByDocumentKey = references;
+}
+
+- (void)performConsistencyCheck {
+ if (self.queue.count == 0) {
+ FSTAssert([self.batchesByDocumentKey isEmpty],
+ @"Document leak -- detected dangling mutation references when queue is empty.");
+ }
+}
+
+#pragma mark - FSTGarbageSource implementation
+
+- (BOOL)containsKey:(FSTDocumentKey *)key {
+ // Create a reference with a zero ID as the start position to find any document reference with
+ // this key.
+ FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0];
+
+ NSEnumerator<FSTDocumentReference *> *enumerator =
+ [self.batchesByDocumentKey objectEnumeratorFrom:reference];
+ FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key;
+ return [firstKey isEqual:key];
+}
+
+#pragma mark - Helpers
+
+/**
+ * A private helper that collects all the mutation batches in the queue up to but not including
+ * the given endIndex. All tombstones in the queue are excluded.
+ */
+- (NSArray<FSTMutationBatch *> *)allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex {
+ NSMutableArray<FSTMutationBatch *> *result = [NSMutableArray arrayWithCapacity:endIndex];
+
+ NSUInteger index = 0;
+ for (FSTMutationBatch *batch in self.queue) {
+ if (index++ >= endIndex) break;
+
+ if (![batch isTombstone]) {
+ [result addObject:batch];
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Finds the index of the given batchID in the mutation queue. This operation is O(1).
+ *
+ * @return The computed index of the batch with the given batchID, based on the state of the
+ * queue. Note this index can negative if the requested batchID has already been removed from
+ * the queue or past the end of the queue if the batchID is larger than the last added batch.
+ */
+- (NSInteger)indexOfBatchID:(FSTBatchID)batchID {
+ NSMutableArray<FSTMutationBatch *> *queue = self.queue;
+ NSUInteger count = queue.count;
+ if (count == 0) {
+ // As an index this is past the end of the queue
+ return 0;
+ }
+
+ // Examine the front of the queue to figure out the difference between the batchID and indexes
+ // in the array. Note that since the queue is ordered by batchID, if the first batch has a larger
+ // batchID then the requested batchID doesn't exist in the queue.
+ FSTMutationBatch *firstBatch = queue[0];
+ FSTBatchID firstBatchID = firstBatch.batchID;
+ return batchID - firstBatchID;
+}
+
+/**
+ * Finds the index of the given batchID in the mutation queue and asserts that the resulting
+ * index is within the bounds of the queue.
+ *
+ * @param batchID The batchID to search for
+ * @param action A description of what the caller is doing, phrased in passive form (e.g.
+ * "acknowledged" in a routine that acknowledges batches).
+ */
+- (NSUInteger)indexOfExistingBatchID:(FSTBatchID)batchID action:(NSString *)action {
+ NSInteger index = [self indexOfBatchID:batchID];
+ FSTAssert(index >= 0 && index < self.queue.count, @"Batches must exist to be %@", action);
+ return (NSUInteger)index;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTMemoryPersistence.h b/Firestore/Source/Local/FSTMemoryPersistence.h
new file mode 100644
index 0000000..c52962a
--- /dev/null
+++ b/Firestore/Source/Local/FSTMemoryPersistence.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>
+
+#import "FSTPersistence.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An in-memory implementation of the FSTPersistence protocol. Values are stored only in RAM and
+ * are never persisted to any durable storage.
+ */
+@interface FSTMemoryPersistence : NSObject <FSTPersistence>
+
++ (instancetype)persistence;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTMemoryPersistence.m b/Firestore/Source/Local/FSTMemoryPersistence.m
new file mode 100644
index 0000000..9caf3e7
--- /dev/null
+++ b/Firestore/Source/Local/FSTMemoryPersistence.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 "FSTMemoryPersistence.h"
+
+#import "FSTAssert.h"
+#import "FSTMemoryMutationQueue.h"
+#import "FSTMemoryQueryCache.h"
+#import "FSTMemoryRemoteDocumentCache.h"
+#import "FSTUser.h"
+#import "FSTWriteGroup.h"
+#import "FSTWriteGroupTracker.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMemoryPersistence ()
+@property(nonatomic, strong, nonnull) FSTWriteGroupTracker *writeGroupTracker;
+@property(nonatomic, strong, nonnull)
+ NSMutableDictionary<FSTUser *, id<FSTMutationQueue>> *mutationQueues;
+@property(nonatomic, assign, getter=isStarted) BOOL started;
+@end
+
+@implementation FSTMemoryPersistence {
+ /**
+ * The FSTQueryCache representing the persisted cache of queries.
+ *
+ * Note that this is retained here to make it easier to write tests affecting both the in-memory
+ * and LevelDB-backed persistence layers. Tests can create a new FSTLocalStore wrapping this
+ * FSTPersistence instance and this will make the in-memory persistence layer behave as if it
+ * were actually persisting values.
+ */
+ FSTMemoryQueryCache *_queryCache;
+
+ /** The FSTRemoteDocumentCache representing the persisted cache of remote documents. */
+ FSTMemoryRemoteDocumentCache *_remoteDocumentCache;
+}
+
++ (instancetype)persistence {
+ return [[FSTMemoryPersistence alloc] init];
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _writeGroupTracker = [FSTWriteGroupTracker tracker];
+ _queryCache = [[FSTMemoryQueryCache alloc] init];
+ _remoteDocumentCache = [[FSTMemoryRemoteDocumentCache alloc] init];
+ _mutationQueues = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+- (BOOL)start:(NSError **)error {
+ // No durable state to read on startup.
+ FSTAssert(!self.isStarted, @"FSTMemoryPersistence double-started!");
+ self.started = YES;
+ return YES;
+}
+
+- (void)shutdown {
+ // No durable state to ensure is closed on shutdown.
+ FSTAssert(self.isStarted, @"FSTMemoryPersistence shutdown without start!");
+ self.started = NO;
+}
+
+- (id<FSTMutationQueue>)mutationQueueForUser:(FSTUser *)user {
+ id<FSTMutationQueue> queue = self.mutationQueues[user];
+ if (!queue) {
+ queue = [FSTMemoryMutationQueue mutationQueue];
+ self.mutationQueues[user] = queue;
+ }
+ return queue;
+}
+
+- (id<FSTQueryCache>)queryCache {
+ return _queryCache;
+}
+
+- (id<FSTRemoteDocumentCache>)remoteDocumentCache {
+ return _remoteDocumentCache;
+}
+
+- (FSTWriteGroup *)startGroupWithAction:(NSString *)action {
+ return [self.writeGroupTracker startGroupWithAction:action];
+}
+
+- (void)commitGroup:(FSTWriteGroup *)group {
+ [self.writeGroupTracker endGroup:group];
+
+ FSTAssert(group.isEmpty, @"Memory persistence shouldn't use write groups: %@", group.action);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.h b/Firestore/Source/Local/FSTMemoryQueryCache.h
new file mode 100644
index 0000000..58e0133
--- /dev/null
+++ b/Firestore/Source/Local/FSTMemoryQueryCache.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 "FSTQueryCache.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An implementation of the FSTQueryCache protocol that merely keeps queries in memory, suitable
+ * for online only clients with persistence disabled.
+ */
+@interface FSTMemoryQueryCache : NSObject <FSTQueryCache>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.m b/Firestore/Source/Local/FSTMemoryQueryCache.m
new file mode 100644
index 0000000..1466caa
--- /dev/null
+++ b/Firestore/Source/Local/FSTMemoryQueryCache.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 "FSTMemoryQueryCache.h"
+
+#import "FSTQuery.h"
+#import "FSTQueryData.h"
+#import "FSTReferenceSet.h"
+#import "FSTSnapshotVersion.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMemoryQueryCache ()
+
+/** Maps a query to the data about that query. */
+@property(nonatomic, strong, readonly) NSMutableDictionary<FSTQuery *, FSTQueryData *> *queries;
+
+/** A ordered bidirectional mapping between documents and the remote target IDs. */
+@property(nonatomic, strong, readonly) FSTReferenceSet *references;
+
+/** The highest numbered target ID encountered. */
+@property(nonatomic, assign) FSTTargetID highestTargetID;
+
+@end
+
+@implementation FSTMemoryQueryCache {
+ /** The last received snapshot version. */
+ FSTSnapshotVersion *_lastRemoteSnapshotVersion;
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _queries = [NSMutableDictionary dictionary];
+ _references = [[FSTReferenceSet alloc] init];
+ _lastRemoteSnapshotVersion = [FSTSnapshotVersion noVersion];
+ }
+ return self;
+}
+
+#pragma mark - FSTQueryCache implementation
+#pragma mark Query tracking
+
+- (void)start {
+ // Nothing to do.
+}
+
+- (void)shutdown {
+ // No resources to release.
+}
+
+- (FSTTargetID)highestTargetID {
+ return _highestTargetID;
+}
+
+- (FSTSnapshotVersion *)lastRemoteSnapshotVersion {
+ return _lastRemoteSnapshotVersion;
+}
+
+- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ group:(FSTWriteGroup *)group {
+ _lastRemoteSnapshotVersion = snapshotVersion;
+}
+
+- (void)addQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group {
+ self.queries[queryData.query] = queryData;
+ if (queryData.targetID > self.highestTargetID) {
+ self.highestTargetID = queryData.targetID;
+ }
+}
+
+- (void)removeQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group {
+ [self.queries removeObjectForKey:queryData.query];
+ [self.references removeReferencesForID:queryData.targetID];
+}
+
+- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query {
+ return self.queries[query];
+}
+
+#pragma mark Reference tracking
+
+- (void)addMatchingKeys:(FSTDocumentKeySet *)keys
+ forTargetID:(FSTTargetID)targetID
+ group:(__unused FSTWriteGroup *)group {
+ [self.references addReferencesToKeys:keys forID:targetID];
+}
+
+- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys
+ forTargetID:(FSTTargetID)targetID
+ group:(__unused FSTWriteGroup *)group {
+ [self.references removeReferencesToKeys:keys forID:targetID];
+}
+
+- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(__unused FSTWriteGroup *)group {
+ [self.references removeReferencesForID:targetID];
+}
+
+- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID {
+ return [self.references referencedKeysForID:targetID];
+}
+
+#pragma mark - FSTGarbageSource implementation
+
+- (nullable id<FSTGarbageCollector>)garbageCollector {
+ return self.references.garbageCollector;
+}
+
+- (void)setGarbageCollector:(nullable id<FSTGarbageCollector>)garbageCollector {
+ self.references.garbageCollector = garbageCollector;
+}
+
+- (BOOL)containsKey:(FSTDocumentKey *)key {
+ return [self.references containsKey:key];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h
new file mode 100644
index 0000000..aca0ca1
--- /dev/null
+++ b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.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 "FSTRemoteDocumentCache.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMemoryRemoteDocumentCache : NSObject <FSTRemoteDocumentCache>
+
+- (instancetype)init NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m
new file mode 100644
index 0000000..175be43
--- /dev/null
+++ b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTMemoryRemoteDocumentCache.h"
+
+#import "FSTDocument.h"
+#import "FSTDocumentDictionary.h"
+#import "FSTDocumentKey.h"
+#import "FSTPath.h"
+#import "FSTQuery.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMemoryRemoteDocumentCache ()
+
+/** Underlying cache of documents. */
+@property(nonatomic, strong) FSTMaybeDocumentDictionary *docs;
+
+@end
+
+@implementation FSTMemoryRemoteDocumentCache
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _docs = [FSTMaybeDocumentDictionary maybeDocumentDictionary];
+ }
+ return self;
+}
+
+- (void)shutdown {
+}
+
+- (void)addEntry:(FSTMaybeDocument *)document group:(FSTWriteGroup *)group {
+ self.docs = [self.docs dictionaryBySettingObject:document forKey:document.key];
+}
+
+- (void)removeEntryForKey:(FSTDocumentKey *)key group:(FSTWriteGroup *)group {
+ self.docs = [self.docs dictionaryByRemovingObjectForKey:key];
+}
+
+- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)key {
+ return self.docs[key];
+}
+
+- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query {
+ FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary];
+
+ // Documents are ordered by key, so we can use a prefix scan to narrow down the documents
+ // we need to match the query against.
+ FSTDocumentKey *prefix = [FSTDocumentKey keyWithPath:[query.path pathByAppendingSegment:@""]];
+ NSEnumerator<FSTDocumentKey *> *enumerator = [self.docs keyEnumeratorFrom:prefix];
+ for (FSTDocumentKey *key in enumerator) {
+ if (![query.path isPrefixOfPath:key.path]) {
+ break;
+ }
+ FSTMaybeDocument *maybeDoc = self.docs[key];
+ if (![maybeDoc isKindOfClass:[FSTDocument class]]) {
+ continue;
+ }
+ FSTDocument *doc = (FSTDocument *)maybeDoc;
+ if ([query matchesDocument:doc]) {
+ result = [result dictionaryBySettingObject:doc forKey:doc.key];
+ }
+ }
+
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTMutationQueue.h b/Firestore/Source/Local/FSTMutationQueue.h
new file mode 100644
index 0000000..c822b96
--- /dev/null
+++ b/Firestore/Source/Local/FSTMutationQueue.h
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTGarbageCollector.h"
+#import "FSTTypes.h"
+
+@class FSTDocumentKey;
+@class FSTMutation;
+@class FSTMutationBatch;
+@class FSTQuery;
+@class FSTTimestamp;
+@class FSTWriteGroup;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTMutationQueue
+
+/** A queue of mutations to apply to the remote store. */
+@protocol FSTMutationQueue <NSObject, FSTGarbageSource>
+
+/**
+ * Starts the mutation queue, performing any initial reads that might be required to establish
+ * invariants, etc.
+ *
+ * After starting, the mutation queue must guarantee that the highestAcknowledgedBatchID is less
+ * than nextBatchID. This prevents the local store from creating new batches that the mutation
+ * queue would consider erroneously acknowledged.
+ */
+- (void)startWithGroup:(FSTWriteGroup *)group;
+
+/** Shuts this mutation queue down, closing open files, etc. */
+- (void)shutdown;
+
+/** Returns YES if this queue contains no mutation batches. */
+- (BOOL)isEmpty;
+
+/**
+ * Returns the next FSTBatchID that will be assigned to a new mutation batch.
+ *
+ * Callers generally don't care about this value except to test that the mutation queue is
+ * properly maintaining the invariant that highestAcknowledgedBatchID is less than nextBatchID.
+ */
+- (FSTBatchID)nextBatchID;
+
+/**
+ * Returns the highest batchID that has been acknowledged. If no batches have been acknowledged
+ * or if there are no batches in the queue this can return kFSTBatchIDUnknown.
+ */
+- (FSTBatchID)highestAcknowledgedBatchID;
+
+/** Acknowledges the given batch. */
+- (void)acknowledgeBatch:(FSTMutationBatch *)batch
+ streamToken:(nullable NSData *)streamToken
+ group:(FSTWriteGroup *)group;
+
+/** Returns the current stream token for this mutation queue. */
+- (nullable NSData *)lastStreamToken;
+
+/** Sets the stream token for this mutation queue. */
+- (void)setLastStreamToken:(nullable NSData *)streamToken group:(FSTWriteGroup *)group;
+
+/** Creates a new mutation batch and adds it to this mutation queue. */
+- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime
+ mutations:(NSArray<FSTMutation *> *)mutations
+ group:(FSTWriteGroup *)group;
+
+/** Loads the mutation batch with the given batchID. */
+- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID;
+
+/**
+ * Gets the first unacknowledged mutation batch after the passed in batchId in the mutation queue
+ * or nil if empty.
+ *
+ * @param batchID The batch to search after, or kFSTBatchIDUnknown for the first mutation in the
+ * queue.
+ *
+ * @return the next mutation or nil if there wasn't one.
+ */
+- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID;
+
+/** Gets all mutation batches in the mutation queue. */
+// TODO(mikelehen): PERF: Current consumer only needs mutated keys; if we can provide that
+// cheaply, we should replace this.
+- (NSArray<FSTMutationBatch *> *)allMutationBatches;
+
+/**
+ * Finds all mutations with a batchID less than or equal to the given batchID.
+ *
+ * Generally the caller should be asking for the next unacknowledged batchID and the number of
+ * acknowledged batches should be very small when things are functioning well.
+ *
+ * @param batchID The batch to search through.
+ *
+ * @return an NSArray containing all batches with matching batchIDs.
+ */
+// TODO(mcg): This should really return NSEnumerator and the caller should be adjusted to only
+// loop through these once.
+- (NSArray<FSTMutationBatch *> *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID;
+
+/**
+ * Finds all mutation batches that could @em possibly affect the given document key. Not all
+ * mutations in a batch will necessarily affect the document key, so when looping through the
+ * batch you'll need to check that the mutation itself matches the key.
+ *
+ * Note that because of this requirement implementations are free to return mutation batches that
+ * don't contain the document key at all if it's convenient.
+ */
+// TODO(mcg): This should really return an NSEnumerator
+// also for b/32992024, all backing stores should really index by document key
+- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKey:
+ (FSTDocumentKey *)documentKey;
+
+/**
+ * Finds all mutation batches that could affect the results for the given query. Not all
+ * mutations in a batch will necessarily affect the query, so when looping through the batch
+ * you'll need to check that the mutation itself matches the query.
+ *
+ * Note that because of this requirement implementations are free to return mutation batches that
+ * don't match the query at all if it's convenient.
+ *
+ * NOTE: A FSTPatchMutation does not need to include all fields in the query filter criteria in
+ * order to be a match (but any fields it does contain do need to match).
+ */
+// TODO(mikelehen): This should perhaps return an NSEnumerator, though I'm not sure we can avoid
+// loading them all in memory.
+- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingQuery:(FSTQuery *)query;
+
+/**
+ * Removes the given mutation batches from the queue. This is useful in two circumstances:
+ *
+ * + Removing applied mutations from the head of the queue
+ * + Removing rejected mutations from anywhere in the queue
+ *
+ * In both cases, the array of mutations to remove must be a contiguous range of batchIds. This is
+ * most easily accomplished by loading mutations with @a -allMutationBatchesThroughBatchID:.
+ */
+- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches group:(FSTWriteGroup *)group;
+
+/** Performs a consistency check, examining the mutation queue for any leaks, if possible. */
+- (void)performConsistencyCheck;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTNoOpGarbageCollector.h b/Firestore/Source/Local/FSTNoOpGarbageCollector.h
new file mode 100644
index 0000000..8873a1b
--- /dev/null
+++ b/Firestore/Source/Local/FSTNoOpGarbageCollector.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 "FSTGarbageCollector.h"
+
+@class FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A garbage collector implementation that does absolutely nothing. It ignores all
+ * addGarbageSource: and addPotentialGarbageKey: messages and never produces any garbage.
+ */
+@interface FSTNoOpGarbageCollector : NSObject <FSTGarbageCollector>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTNoOpGarbageCollector.m b/Firestore/Source/Local/FSTNoOpGarbageCollector.m
new file mode 100644
index 0000000..6e035ab
--- /dev/null
+++ b/Firestore/Source/Local/FSTNoOpGarbageCollector.m
@@ -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 "FSTNoOpGarbageCollector.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTNoOpGarbageCollector
+
+- (BOOL)isEager {
+ return NO;
+}
+
+- (void)addGarbageSource:(id<FSTGarbageSource>)garbageSource {
+ // Not tracking garbage so don't track sources.
+}
+
+- (void)removeGarbageSource:(id<FSTGarbageSource>)garbageSource {
+ // Not tracking garbage so don't track sources.
+}
+
+- (void)addPotentialGarbageKey:(FSTDocumentKey *)key {
+ // Not tracking garbage so ignore.
+}
+
+- (NSSet<FSTDocumentKey *> *)collectGarbage {
+ return [NSSet set];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTPersistence.h b/Firestore/Source/Local/FSTPersistence.h
new file mode 100644
index 0000000..cf07a9e
--- /dev/null
+++ b/Firestore/Source/Local/FSTPersistence.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 FSTUser;
+@class FSTWriteGroup;
+@protocol FSTMutationQueue;
+@protocol FSTQueryCache;
+@protocol FSTRemoteDocumentCache;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTPersistence is the lowest-level shared interface to persistent storage in Firestore.
+ *
+ * FSTPersistence is used to create FSTMutationQueue and FSTRemoteDocumentCache instances backed
+ * by persistence (which might be in-memory or LevelDB).
+ *
+ * FSTPersistence also exposes an API to create and commit FSTWriteGroup instances.
+ * Implementations of FSTWriteGroup/FSTPersistence only need to guarantee that writes made
+ * against the FSTWriteGroup are not made to durable storage until commitGroup:action: is called
+ * here. Since memory-only storage components do not alter durable storage, they are free to ignore
+ * the group.
+ *
+ * This contract is enough to allow the FSTLocalStore be be written independently of whether or not
+ * the stored state actually is durably persisted. If persistent storage is enabled, writes are
+ * grouped together to avoid inconsistent state that could cause crashes.
+ *
+ * Concretely, when persistent storage is enabled, the persistent versions of FSTMutationQueue,
+ * FSTRemoteDocumentCache, and others (the mutators) will defer their writes into an FSTWriteGroup.
+ * Once the local store has completed one logical operation, it commits the write group using
+ * [FSTPersistence commitGroup:action:].
+ *
+ * When persistent storage is disabled, the non-persistent versions of the mutators ignore the
+ * FSTWriteGroup and [FSTPersistence commitGroup:action:] is a no-op. This short-cut is allowed
+ * because memory-only storage leaves no state so it cannot be inconsistent.
+ *
+ * This simplifies the implementations of the mutators and allows memory-only implementations to
+ * supplement the persistent ones without requiring any special dual-store implementation of
+ * FSTPersistence. The cost is that the FSTLocalStore needs to be slightly careful about the order
+ * of its reads and writes in order to avoid relying on being able to read back uncommitted writes.
+ */
+@protocol FSTPersistence <NSObject>
+
+/**
+ * Starts persistent storage, opening the database or similar.
+ *
+ * @param error An error object that will be populated if startup fails.
+ * @return YES if persistent storage started successfully, NO otherwise.
+ */
+- (BOOL)start:(NSError **)error;
+
+/** Releases any resources held during eager shutdown. */
+- (void)shutdown;
+
+/**
+ * Returns an FSTMutationQueue representing the persisted mutations for the given user.
+ *
+ * <p>Note: The implementation is free to return the same instance every time this is called for a
+ * given user. In particular, the memory-backed implementation does this to emulate the persisted
+ * implementation to the extent possible (e.g. in the case of uid switching from
+ * sally=>jack=>sally, sally's mutation queue will be preserved).
+ */
+- (id<FSTMutationQueue>)mutationQueueForUser:(FSTUser *)user;
+
+/** Creates an FSTQueryCache representing the persisted cache of queries. */
+- (id<FSTQueryCache>)queryCache;
+
+/** Creates an FSTRemoteDocumentCache representing the persisted cache of remote documents. */
+- (id<FSTRemoteDocumentCache>)remoteDocumentCache;
+
+/**
+ * Creates an FSTWriteGroup with the specified action description.
+ *
+ * @param action A description of the action performed by this group, used for logging.
+ * @return The created group.
+ */
+- (FSTWriteGroup *)startGroupWithAction:(NSString *)action;
+
+/**
+ * Commits all accumulated changes in the given group. If there are no changes this is a no-op.
+ *
+ * @param group The group of changes to write as a unit.
+ */
+- (void)commitGroup:(FSTWriteGroup *)group;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTQueryCache.h b/Firestore/Source/Local/FSTQueryCache.h
new file mode 100644
index 0000000..87ee342
--- /dev/null
+++ b/Firestore/Source/Local/FSTQueryCache.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTDocumentKeySet.h"
+#import "FSTGarbageCollector.h"
+#import "FSTTypes.h"
+
+@class FSTDocumentKey;
+@class FSTDocumentSet;
+@class FSTMaybeDocument;
+@class FSTQuery;
+@class FSTQueryData;
+@class FSTWriteGroup;
+@class FSTSnapshotVersion;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Represents cached queries received from the remote backend. This contains both a mapping between
+ * queries and the documents that matched them according to the server, but also metadata about the
+ * queries.
+ *
+ * The cache is keyed by FSTQuery and entries in the cache are FSTQueryData instances.
+ */
+@protocol FSTQueryCache <NSObject, FSTGarbageSource>
+
+/** Starts the query cache up. */
+- (void)start;
+
+/** Shuts this cache down, closing open files, etc. */
+- (void)shutdown;
+
+/**
+ * Returns the highest target ID of any query in the cache. Typically called during startup to
+ * seed a target ID generator and avoid collisions with existing queries. If there are no queries
+ * in the cache, returns zero.
+ */
+- (FSTTargetID)highestTargetID;
+
+/**
+ * A global snapshot version representing the last consistent snapshot we received from the
+ * backend. This is monotonically increasing and any snapshots received from the backend prior to
+ * this version (e.g. for targets resumed with a resume_token) should be suppressed (buffered)
+ * until the backend has caught up to this snapshot version again. This prevents our cache from
+ * ever going backwards in time.
+ *
+ * This is updated whenever our we get a TargetChange with a read_time and empty target_ids.
+ */
+- (FSTSnapshotVersion *)lastRemoteSnapshotVersion;
+
+/**
+ * Set the snapshot version representing the last consistent snapshot received from the backend.
+ * (see -lastRemoteSnapshotVersion for more details).
+ *
+ * @param snapshotVersion The new snapshot version.
+ */
+- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ group:(FSTWriteGroup *)group;
+
+/**
+ * Adds or replaces an entry in the cache.
+ *
+ * The cache key is extracted from `queryData.query`. If there is already a cache entry for the
+ * key, it will be replaced.
+ *
+ * @param queryData An FSTQueryData instance to put in the cache.
+ */
+- (void)addQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group;
+
+/** Removes the cached entry for the given query data (no-op if no entry exists). */
+- (void)removeQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group;
+
+/**
+ * Looks up an FSTQueryData entry in the cache.
+ *
+ * @param query The query corresponding to the entry to look up.
+ * @return The cached FSTQueryData entry, or nil if the cache has no entry for the query.
+ */
+- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query;
+
+/** Adds the given document keys to cached query results of the given target ID. */
+- (void)addMatchingKeys:(FSTDocumentKeySet *)keys
+ forTargetID:(FSTTargetID)targetID
+ group:(FSTWriteGroup *)group;
+
+/** Removes the given document keys from the cached query results of the given target ID. */
+- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys
+ forTargetID:(FSTTargetID)targetID
+ group:(FSTWriteGroup *)group;
+
+/** Removes all the keys in the query results of the given target ID. */
+- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(FSTWriteGroup *)group;
+
+- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTQueryData.h b/Firestore/Source/Local/FSTQueryData.h
new file mode 100644
index 0000000..060fd78
--- /dev/null
+++ b/Firestore/Source/Local/FSTQueryData.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTTypes.h"
+
+@class FSTQuery;
+@class FSTSnapshotVersion;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** An enumeration of the different purposes we have for queries. */
+typedef NS_ENUM(NSInteger, FSTQueryPurpose) {
+ /** A regular, normal query. */
+ FSTQueryPurposeListen,
+
+ /** The query was used to refill a query after an existence filter mismatch. */
+ FSTQueryPurposeExistenceFilterMismatch,
+
+ /** The query was used to resolve a limbo document. */
+ FSTQueryPurposeLimboResolution,
+};
+
+/** An immutable set of metadata that the store will need to keep track of for each query. */
+@interface FSTQueryData : NSObject
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ targetID:(FSTTargetID)targetID
+ purpose:(FSTQueryPurpose)purpose
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ resumeToken:(NSData *)resumeToken NS_DESIGNATED_INITIALIZER;
+
+/** Convenience initializer for use when creating an FSTQueryData for the first time. */
+- (instancetype)initWithQuery:(FSTQuery *)query
+ targetID:(FSTTargetID)targetID
+ purpose:(FSTQueryPurpose)purpose;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** Creates a new query data instance with an updated snapshot version and resume token. */
+- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ resumeToken:(NSData *)resumeToken;
+
+/** The query being listened to. */
+@property(nonatomic, strong, readonly) FSTQuery *query;
+
+/**
+ * The targetID to which the query corresponds, assigned by the FSTLocalStore for user queries or
+ * the FSTSyncEngine for limbo queries.
+ */
+@property(nonatomic, assign, readonly) FSTTargetID targetID;
+
+/** The purpose of the query. */
+@property(nonatomic, assign, readonly) FSTQueryPurpose purpose;
+
+/** The latest snapshot version seen for this target. */
+@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion;
+
+/**
+ * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting
+ * without retransmitting all the data that matches the query. The resume token essentially
+ * identifies a point in time from which the server should resume sending results.
+ */
+@property(nonatomic, copy, readonly) NSData *resumeToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTQueryData.m b/Firestore/Source/Local/FSTQueryData.m
new file mode 100644
index 0000000..438f229
--- /dev/null
+++ b/Firestore/Source/Local/FSTQueryData.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 "FSTQueryData.h"
+
+#import "FSTQuery.h"
+#import "FSTSnapshotVersion.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTQueryData
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ targetID:(FSTTargetID)targetID
+ purpose:(FSTQueryPurpose)purpose
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ resumeToken:(NSData *)resumeToken {
+ self = [super init];
+ if (self) {
+ _query = query;
+ _targetID = targetID;
+ _purpose = purpose;
+ _snapshotVersion = snapshotVersion;
+ _resumeToken = [resumeToken copy];
+ }
+ return self;
+}
+
+- (instancetype)initWithQuery:(FSTQuery *)query
+ targetID:(FSTTargetID)targetID
+ purpose:(FSTQueryPurpose)purpose {
+ return [self initWithQuery:query
+ targetID:targetID
+ purpose:purpose
+ snapshotVersion:[FSTSnapshotVersion noVersion]
+ resumeToken:[NSData data]];
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+ if (![object isKindOfClass:[FSTQueryData class]]) {
+ return NO;
+ }
+
+ FSTQueryData *other = (FSTQueryData *)object;
+ return [self.query isEqual:other.query] && self.targetID == other.targetID &&
+ self.purpose == other.purpose && [self.snapshotVersion isEqual:other.snapshotVersion] &&
+ [self.resumeToken isEqual:other.resumeToken];
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.query hash];
+ result = result * 31 + self.targetID;
+ result = result * 31 + self.purpose;
+ result = result * 31 + [self.snapshotVersion hash];
+ result = result * 31 + [self.resumeToken hash];
+ return result;
+}
+
+- (NSString *)description {
+ return [NSString
+ stringWithFormat:@"<FSTQueryData: query:%@ target:%d purpose:%lu version:%@ resumeToken:%@)>",
+ self.query, self.targetID, (unsigned long)self.purpose, self.snapshotVersion,
+ self.resumeToken];
+}
+
+- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ resumeToken:(NSData *)resumeToken {
+ return [[FSTQueryData alloc] initWithQuery:self.query
+ targetID:self.targetID
+ purpose:self.purpose
+ snapshotVersion:snapshotVersion
+ resumeToken:resumeToken];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTReferenceSet.h b/Firestore/Source/Local/FSTReferenceSet.h
new file mode 100644
index 0000000..e4f50a7
--- /dev/null
+++ b/Firestore/Source/Local/FSTReferenceSet.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>
+
+#import "FSTDocumentKeySet.h"
+#import "FSTGarbageCollector.h"
+#import "FSTTypes.h"
+
+@class FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A collection of references to a document from some kind of numbered entity (either a targetID or
+ * batchID). As references are added to or removed from the set corresponding events are emitted to
+ * a registered garbage collector.
+ *
+ * Each reference is represented by a FSTDocumentReference object. Each of them contains enough
+ * information to uniquely identify the reference. They are all stored primarily in a set sorted
+ * by key. A document is considered garbage if there's no references in that set (this can be
+ * efficiently checked thanks to sorting by key).
+ *
+ * FSTReferenceSet also keeps a secondary set that contains references sorted by IDs. This one is
+ * used to efficiently implement removal of all references by some target ID.
+ */
+@interface FSTReferenceSet : NSObject <FSTGarbageSource>
+
+/** Keeps track of keys that have references. */
+@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector;
+
+/** Returns YES if the reference set contains no references. */
+- (BOOL)isEmpty;
+
+/** Adds a reference to the given document key for the given ID. */
+- (void)addReferenceToKey:(FSTDocumentKey *)key forID:(int)ID;
+
+/** Add references to the given document keys for the given ID. */
+- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID;
+
+/** Removes a reference to the given document key for the given ID. */
+- (void)removeReferenceToKey:(FSTDocumentKey *)key forID:(int)ID;
+
+/** Removes references to the given document keys for the given ID. */
+- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID;
+
+/** Clears all references with a given ID. Calls -removeReferenceToKey: for each key removed. */
+- (void)removeReferencesForID:(int)ID;
+
+/** Clears all references for all IDs. */
+- (void)removeAllReferences;
+
+/** Returns all of the document keys that have had references added for the given ID. */
+- (FSTDocumentKeySet *)referencedKeysForID:(int)ID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTReferenceSet.m b/Firestore/Source/Local/FSTReferenceSet.m
new file mode 100644
index 0000000..2326ded
--- /dev/null
+++ b/Firestore/Source/Local/FSTReferenceSet.m
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTReferenceSet.h"
+
+#import "FSTDocumentKey.h"
+#import "FSTDocumentReference.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTReferenceSet
+
+@interface FSTReferenceSet ()
+
+/** A set of outstanding references to a document sorted by key. */
+@property(nonatomic, strong) FSTImmutableSortedSet<FSTDocumentReference *> *referencesByKey;
+
+/** A set of outstanding references to a document sorted by target ID (or batch ID). */
+@property(nonatomic, strong) FSTImmutableSortedSet<FSTDocumentReference *> *referencesByID;
+
+@end
+
+@implementation FSTReferenceSet
+
+#pragma mark - Initializer
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _referencesByKey =
+ [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey];
+ _referencesByID = [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByID];
+ }
+ return self;
+}
+
+#pragma mark - Testing helper methods
+
+- (BOOL)isEmpty {
+ return [self.referencesByKey isEmpty];
+}
+
+- (NSUInteger)count {
+ return self.referencesByKey.count;
+}
+
+#pragma mark - Public methods
+
+- (void)addReferenceToKey:(FSTDocumentKey *)key forID:(int)ID {
+ FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:ID];
+ self.referencesByKey = [self.referencesByKey setByAddingObject:reference];
+ self.referencesByID = [self.referencesByID setByAddingObject:reference];
+}
+
+- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID {
+ [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ [self addReferenceToKey:key forID:ID];
+ }];
+}
+
+- (void)removeReferenceToKey:(FSTDocumentKey *)key forID:(int)ID {
+ [self removeReference:[[FSTDocumentReference alloc] initWithKey:key ID:ID]];
+}
+
+- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID {
+ [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ [self removeReferenceToKey:key forID:ID];
+ }];
+}
+
+- (void)removeReferencesForID:(int)ID {
+ FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]];
+ FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID];
+ FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)];
+
+ [self.referencesByID enumerateObjectsFrom:start
+ to:end
+ usingBlock:^(FSTDocumentReference *reference, BOOL *stop) {
+ [self removeReference:reference];
+ }];
+}
+
+- (void)removeAllReferences {
+ for (FSTDocumentReference *reference in self.referencesByKey.objectEnumerator) {
+ [self removeReference:reference];
+ }
+}
+
+- (void)removeReference:(FSTDocumentReference *)reference {
+ self.referencesByKey = [self.referencesByKey setByRemovingObject:reference];
+ self.referencesByID = [self.referencesByID setByRemovingObject:reference];
+ [self.garbageCollector addPotentialGarbageKey:reference.key];
+}
+
+- (FSTDocumentKeySet *)referencedKeysForID:(int)ID {
+ FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]];
+ FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID];
+ FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)];
+
+ __block FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet];
+ [self.referencesByID enumerateObjectsFrom:start
+ to:end
+ usingBlock:^(FSTDocumentReference *reference, BOOL *stop) {
+ keys = [keys setByAddingObject:reference.key];
+ }];
+ return keys;
+}
+
+- (BOOL)containsKey:(FSTDocumentKey *)key {
+ // Create a reference with a zero ID as the start position to find any document reference with
+ // this key.
+ FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0];
+
+ NSEnumerator<FSTDocumentReference *> *enumerator =
+ [self.referencesByKey objectEnumeratorFrom:reference];
+ FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key;
+ return [firstKey isEqual:reference.key];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTRemoteDocumentCache.h b/Firestore/Source/Local/FSTRemoteDocumentCache.h
new file mode 100644
index 0000000..8979455
--- /dev/null
+++ b/Firestore/Source/Local/FSTRemoteDocumentCache.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 "FSTDocumentDictionary.h"
+
+@class FSTDocumentKey;
+@class FSTMaybeDocument;
+@class FSTQuery;
+@class FSTWriteGroup;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Represents cached documents received from the remote backend.
+ *
+ * The cache is keyed by FSTDocumentKey and entries in the cache are FSTMaybeDocument instances,
+ * meaning we can cache both FSTDocument instances (an actual document with data) as well as
+ * FSTDeletedDocument instances (indicating that the document is known to not exist).
+ */
+@protocol FSTRemoteDocumentCache <NSObject>
+
+/** Shuts this cache down, closing open files, etc. */
+- (void)shutdown;
+
+/**
+ * Adds or replaces an entry in the cache.
+ *
+ * The cache key is extracted from `maybeDocument.key`. If there is already a cache entry for
+ * the key, it will be replaced.
+ *
+ * @param maybeDocument A FSTDocument or FSTDeletedDocument to put in the cache.
+ */
+- (void)addEntry:(FSTMaybeDocument *)maybeDocument group:(FSTWriteGroup *)group;
+
+/** Removes the cached entry for the given key (no-op if no entry exists). */
+- (void)removeEntryForKey:(FSTDocumentKey *)documentKey group:(FSTWriteGroup *)group;
+
+/**
+ * Looks up an entry in the cache.
+ *
+ * @param documentKey The key of the entry to look up.
+ * @return The cached FSTDocument or FSTDeletedDocument entry, or nil if we have nothing cached.
+ */
+- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey;
+
+/**
+ * Executes a query against the cached FSTDocument entries
+ *
+ * Implementations may return extra documents if convenient. The results should be re-filtered
+ * by the consumer before presenting them to the user.
+ *
+ * Cached FSTDeletedDocument entries have no bearing on query results.
+ *
+ * @param query The query to match documents against.
+ * @return The set of matching documents.
+ */
+- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h
new file mode 100644
index 0000000..be0d609
--- /dev/null
+++ b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol FSTRemoteDocumentCache;
+@class FSTMaybeDocument;
+@class FSTDocumentKey;
+@class FSTWriteGroup;
+
+/**
+ * An in-memory buffer of entries to be written to an FSTRemoteDocumentCache. It can be used to
+ * batch up a set of changes to be written to the cache, but additionally supports reading entries
+ * back with the `entryForKey:` method, falling back to the underlying FSTRemoteDocumentCache if
+ * no entry is buffered. In the absence of LevelDB transactions (that would allow reading back
+ * uncommitted writes), this greatly simplifies the implementation of complex operations that
+ * may want to freely read/write entries to the FSTRemoteDocumentCache while still ensuring that
+ * the final writing of the buffered entries is atomic.
+ *
+ * For doing blind writes that don't depend on the current state of the FSTRemoteDocumentCache
+ * or for plain reads, you can/should still just use the FSTRemoteDocumentCache directly.
+ */
+@interface FSTRemoteDocumentChangeBuffer : NSObject
+
++ (instancetype)changeBufferWithCache:(id<FSTRemoteDocumentCache>)cache;
+
+- (instancetype)init __attribute__((unavailable("Use a static constructor instead")));
+
+/** Buffers an `FSTRemoteDocumentCache addEntry:group:` call. */
+- (void)addEntry:(FSTMaybeDocument *)maybeDocument;
+
+// NOTE: removeEntryForKey: is not presently necessary and so is omitted.
+
+/**
+ * Looks up an entry in the cache. The buffered changes will first be checked, and if no
+ * buffered change applies, this will forward to `FSTRemoteDocumentCache entryForKey:`.
+ *
+ * @param documentKey The key of the entry to look up.
+ * @return The cached FSTDocument or FSTDeletedDocument entry, or nil if we have nothing cached.
+ */
+- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey;
+
+/**
+ * Applies buffered changes to the underlying FSTRemoteDocumentCache, using the provided
+ * FSTWriteGroup.
+ */
+- (void)applyToWriteGroup:(FSTWriteGroup *)group;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m
new file mode 100644
index 0000000..12a68ff
--- /dev/null
+++ b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTRemoteDocumentChangeBuffer.h"
+
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTRemoteDocumentCache.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTRemoteDocumentChangeBuffer ()
+
+- (instancetype)initWithCache:(id<FSTRemoteDocumentCache>)cache;
+
+/** The underlying cache we're buffering changes for. */
+@property(nonatomic, strong, nonnull) id<FSTRemoteDocumentCache> remoteDocumentCache;
+
+/** The buffered changes, stored as a dictionary for easy lookups. */
+@property(nonatomic, strong, nullable)
+ NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *changes;
+
+@end
+
+@implementation FSTRemoteDocumentChangeBuffer
+
++ (instancetype)changeBufferWithCache:(id<FSTRemoteDocumentCache>)cache {
+ return [[FSTRemoteDocumentChangeBuffer alloc] initWithCache:cache];
+}
+
+- (instancetype)initWithCache:(id<FSTRemoteDocumentCache>)cache {
+ if (self = [super init]) {
+ _remoteDocumentCache = cache;
+ _changes = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+- (void)addEntry:(FSTMaybeDocument *)maybeDocument {
+ [self assertValid];
+
+ self.changes[maybeDocument.key] = maybeDocument;
+}
+
+- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey {
+ [self assertValid];
+
+ FSTMaybeDocument *bufferedEntry = self.changes[documentKey];
+ if (bufferedEntry) {
+ return bufferedEntry;
+ } else {
+ return [self.remoteDocumentCache entryForKey:documentKey];
+ }
+}
+
+- (void)applyToWriteGroup:(FSTWriteGroup *)group {
+ [self assertValid];
+
+ [self.changes enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTMaybeDocument *value,
+ BOOL *stop) {
+ [self.remoteDocumentCache addEntry:value group:group];
+ }];
+
+ // We should not be used to buffer any more changes.
+ self.changes = nil;
+}
+
+- (void)assertValid {
+ FSTAssert(self.changes, @"Changes have already been applied.");
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTWriteGroup.h b/Firestore/Source/Local/FSTWriteGroup.h
new file mode 100644
index 0000000..21482af
--- /dev/null
+++ b/Firestore/Source/Local/FSTWriteGroup.h
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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>
+
+#ifdef __cplusplus
+#include <memory>
+
+#include "StringView.h"
+
+namespace leveldb {
+class DB;
+class Status;
+}
+
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class GPBMessage;
+
+/**
+ * A group of writes that will be applied together atomically to persistent storage.
+ *
+ * This class is usable by both Objective-C and Objective-C++ clients. Objective-C clients are able
+ * to create a new group and commit it. Objective-C++ clients can additionally add to the group
+ * using deleteKey: and putKey:value:.
+ *
+ * Note that this is a write "group" even though the underlying LevelDB concept is a write "batch"
+ * because Firestore already has a concept of mutation batches, which are user-specified groups of
+ * changes. This means that an FSTWriteGroup may contain the application of multiple user-specified
+ * mutation batches.
+ */
+@interface FSTWriteGroup : NSObject
+
+/**
+ * Creates a new, empty write group.
+ *
+ * @param action A description of the action performed by this group, used for logging.
+ */
++ (instancetype)groupWithAction:(NSString *)action;
+
+- (instancetype)init __attribute__((unavailable("Use a static constructor instead")));
+
+/** The action description assigned to this write group. */
+@property(nonatomic, copy, readonly) NSString *action;
+
+/** Returns YES if the write group has no messages in it. */
+- (BOOL)isEmpty;
+
+#ifdef __cplusplus
+
+/**
+ * Marks the given key for deletion.
+ *
+ * @param key The LevelDB key of the row to delete
+ */
+- (void)removeMessageForKey:(Firestore::StringView)key;
+
+/**
+ * Sets the row identified by the given key to the value of the given protocol buffer message.
+ *
+ * @param key The LevelDB Key of the row to set.
+ * @param message The protocol buffer message whose serialized contents should be used for the
+ * value associated with the key.
+ */
+- (void)setMessage:(GPBMessage *)message forKey:(Firestore::StringView)key;
+
+/**
+ * Sets the row identified by the given key to the value of the given data bytes.
+ *
+ * @param key The LevelDB Key of the row to set.
+ * @param data The exact value to be associated with the key.
+ */
+- (void)setData:(Firestore::StringView)data forKey:(Firestore::StringView)key;
+
+/** Writes the contents to the given LevelDB. */
+- (leveldb::Status)writeToDB:(std::shared_ptr<leveldb::DB>)db;
+
+#endif
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTWriteGroup.mm b/Firestore/Source/Local/FSTWriteGroup.mm
new file mode 100644
index 0000000..e6da131
--- /dev/null
+++ b/Firestore/Source/Local/FSTWriteGroup.mm
@@ -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 "FSTWriteGroup.h"
+
+#import <Protobuf/GPBProtocolBuffers.h>
+#include <leveldb/db.h>
+#include <leveldb/write_batch.h>
+
+#import "FSTLevelDBKey.h"
+#import "FSTAssert.h"
+
+#include "ordered_code.h"
+
+using Firestore::OrderedCode;
+using Firestore::StringView;
+using leveldb::DB;
+using leveldb::Slice;
+using leveldb::Status;
+using leveldb::WriteBatch;
+using leveldb::WriteOptions;
+
+NS_ASSUME_NONNULL_BEGIN
+
+namespace Firestore {
+
+/**
+ * A WriteBatch::Handler implementation that extracts batch details from a leveldb::WriteBatch.
+ * This is used for describing a write batch primarily in log messages after a failure.
+ */
+class BatchDescription : public WriteBatch::Handler {
+ public:
+ BatchDescription() : ops_(0), size_(0), message_([NSMutableString string]) {}
+ virtual ~BatchDescription();
+ virtual void Put(const Slice &key, const Slice &value);
+ virtual void Delete(const Slice &key);
+
+ // Converts the batch to a printable string description of it
+ NSString *ToString() const {
+ return [NSString
+ stringWithFormat:@"%d changes (%lu bytes):%@", ops_, (unsigned long)size_, message_];
+ }
+
+ // Disallow copies and moves
+ BatchDescription(const BatchDescription &) = delete;
+ BatchDescription &operator=(const BatchDescription &) = delete;
+ BatchDescription(BatchDescription &&) = delete;
+ BatchDescription &operator=(BatchDescription &&) = delete;
+
+ private:
+ int ops_;
+ size_t size_;
+ NSMutableString *message_;
+};
+
+BatchDescription::~BatchDescription() {}
+
+void BatchDescription::Put(const Slice &key, const Slice &value) {
+ ops_ += 1;
+ size_ += value.size();
+
+ [message_ appendFormat:@"\n - Put %@ (%lu bytes)", [FSTLevelDBKey descriptionForKey:key],
+ (unsigned long)value.size()];
+}
+
+void BatchDescription::Delete(const Slice &key) {
+ ops_ += 1;
+
+ [message_ appendFormat:@"\n - Delete %@", [FSTLevelDBKey descriptionForKey:key]];
+}
+
+} // namespace Firestore
+
+@interface FSTWriteGroup ()
+- (instancetype)initWithAction:(NSString *)action NS_DESIGNATED_INITIALIZER;
+@end
+
+@implementation FSTWriteGroup {
+ int _changes;
+ WriteBatch _contents;
+}
+
++ (instancetype)groupWithAction:(NSString *)action {
+ return [[FSTWriteGroup alloc] initWithAction:action];
+}
+
+- (instancetype)initWithAction:(NSString *)action {
+ if (self = [super init]) {
+ _action = action;
+ }
+ return self;
+}
+
+- (NSString *)description {
+ Firestore::BatchDescription description;
+ Status status = _contents.Iterate(&description);
+ if (!status.ok()) {
+ FSTFail(@"Iterate over write batch should not fail");
+ }
+ return [NSString
+ stringWithFormat:@"<FSTWriteGroup for %@: %@>", self.action, description.ToString()];
+}
+
+- (void)removeMessageForKey:(StringView)key {
+ _contents.Delete(key);
+ _changes += 1;
+}
+
+- (void)setMessage:(GPBMessage *)message forKey:(StringView)key {
+ NSData *data = [message data];
+ Slice value((const char *)data.bytes, data.length);
+
+ _contents.Put(key, value);
+ _changes += 1;
+}
+
+- (void)setData:(StringView)data forKey:(StringView)key {
+ _contents.Put(key, data);
+ _changes += 1;
+}
+
+- (leveldb::Status)writeToDB:(std::shared_ptr<leveldb::DB>)db {
+ return db->Write(leveldb::WriteOptions(), &_contents);
+}
+
+- (BOOL)isEmpty {
+ return _changes == 0;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTWriteGroupTracker.h b/Firestore/Source/Local/FSTWriteGroupTracker.h
new file mode 100644
index 0000000..bd26a46
--- /dev/null
+++ b/Firestore/Source/Local/FSTWriteGroupTracker.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>
+
+@class FSTWriteGroup;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Helper class for FSTPersistence implementations to create WriteGroups and verify internal
+ * contracts are maintained:
+ * 1. Can't create a group when an uncommitted group exists (no nesting).
+ * 2. Can't commit a group that differs from the last created one.
+ */
+@interface FSTWriteGroupTracker : NSObject
+
+/** Creates and returns an FSTWriteGroupTracker instance. */
++ (instancetype)tracker;
+
+/**
+ * Verifies there's no active group already and then creates a new group and stores it for later
+ * validation with `endGroup`.
+ */
+- (FSTWriteGroup *)startGroupWithAction:(NSString *)action;
+
+/** Ends a group previously started with `startGroupWithAction`. */
+- (void)endGroup:(FSTWriteGroup *)group;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/FSTWriteGroupTracker.m b/Firestore/Source/Local/FSTWriteGroupTracker.m
new file mode 100644
index 0000000..1c6c84d
--- /dev/null
+++ b/Firestore/Source/Local/FSTWriteGroupTracker.m
@@ -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 "FSTWriteGroupTracker.h"
+
+#import "FSTAssert.h"
+#import "FSTWriteGroup.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTWriteGroupTracker ()
+@property(nonatomic, strong, nullable) FSTWriteGroup *activeGroup;
+@end
+
+@implementation FSTWriteGroupTracker
+
++ (instancetype)tracker {
+ return [[FSTWriteGroupTracker alloc] init];
+}
+
+- (FSTWriteGroup *)startGroupWithAction:(NSString *)action {
+ // NOTE: We can relax this to allow nesting if/when we find we need it.
+ FSTAssert(!self.activeGroup,
+ @"Attempt to create write group (%@) while existing write group (%@) still active.",
+ action, self.activeGroup.action);
+ self.activeGroup = [FSTWriteGroup groupWithAction:action];
+ return self.activeGroup;
+}
+
+- (void)endGroup:(FSTWriteGroup *)group {
+ FSTAssert(self.activeGroup == group,
+ @"Attempted to end write group (%@) which is different from active group (%@)",
+ group.action, self.activeGroup.action);
+ self.activeGroup = nil;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Local/StringView.h b/Firestore/Source/Local/StringView.h
new file mode 100644
index 0000000..799baf8
--- /dev/null
+++ b/Firestore/Source/Local/StringView.h
@@ -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.
+ */
+
+#ifndef IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_
+#define IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_
+
+#ifndef __cplusplus
+#error "StringView is Objective-C++ and can only be included from .mm files"
+#endif
+
+#import <Foundation/Foundation.h>
+
+#include <leveldb/slice.h>
+#include <string>
+
+namespace Firestore {
+
+// A simple wrapper for the character data of any string-like type to which
+// we'd like to temporarily refer as an argument.
+//
+// This is superficially similar to StringPiece and leveldb::Slice except
+// that it also supports implicit conversion from NSString *, which is useful
+// when writing Objective-C++ methods that accept any string-like type.
+//
+// Note that much like any other view-type class in C++, the caller is
+// responsible for ensuring that the lifetime of the string-like data is longer
+// than the lifetime of the StringView.
+//
+// Functions that take a StringView argument promise that they won't keep the
+// pointer beyond the immediate scope of their own stack frame.
+class StringView {
+ public:
+ // Creates a StringView from an NSString. When StringView is an argument type
+ // into which an NSString* is passed, the caller should ensure that the
+ // NSString is retained.
+ StringView(NSString *str) : data_([str UTF8String]), size_(str.length) {
+ }
+
+ // Creates a StringView from the given char* pointer with an explicit size.
+ // The character data can contain NUL bytes as a result.
+ StringView(const char *data, size_t size) : data_(data), size_(size) {
+ }
+
+ // Creates a StringView from the given char* pointer but computes the size
+ // with strlen. This is really only suitable for passing C string literals.
+ StringView(const char *data) : data_(data), size_(strlen(data)) {
+ }
+
+ // Creates a StringView from the given slice.
+ StringView(leveldb::Slice slice) : data_(slice.data()), size_(slice.size()) {
+ }
+
+ // Creates a StringView from the given std::string. The string must be an
+ // lvalue for the lifetime requirements to be satisfied.
+ StringView(const std::string &str) : data_(str.data()), size_(str.size()) {
+ }
+
+ // Converts this StringView to a Slice, which is an equivalent (and more
+ // functional) type. The returned slice has the same lifetime as this
+ // StringView.
+ operator leveldb::Slice() {
+ return leveldb::Slice(data_, size_);
+ }
+
+ private:
+ const char *data_;
+ const size_t size_;
+};
+
+} // namespace Firestore
+
+#endif // IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_
diff --git a/Firestore/Source/Model/FSTDatabaseID.h b/Firestore/Source/Model/FSTDatabaseID.h
new file mode 100644
index 0000000..442e764
--- /dev/null
+++ b/Firestore/Source/Model/FSTDatabaseID.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** FSTDatabaseID represents a particular database in the datastore. */
+@interface FSTDatabaseID : NSObject
+
+/**
+ * Creates and returns a new FSTDatabaseID.
+ * @param projectID The project for the database.
+ * @param databaseID The database in the project to use.
+ * @return A new instance of FSTDatabaseID.
+ */
++ (instancetype)databaseIDWithProject:(NSString *)projectID database:(NSString *)databaseID;
+
+/** The project. */
+@property(nonatomic, copy, readonly) NSString *projectID;
+
+/** The database. */
+@property(nonatomic, copy, readonly) NSString *databaseID;
+
+/** Whether this is the default database of the project. */
+- (BOOL)isDefaultDatabase;
+
+- (NSComparisonResult)compare:(FSTDatabaseID *)other;
+- (BOOL)isEqualToDatabaseId:(FSTDatabaseID *)databaseID;
+
+@end
+
+extern NSString *const kDefaultDatabaseID;
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDatabaseID.m b/Firestore/Source/Model/FSTDatabaseID.m
new file mode 100644
index 0000000..bf4b417
--- /dev/null
+++ b/Firestore/Source/Model/FSTDatabaseID.m
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTDatabaseID.h"
+
+#import "FSTAssert.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** The default name for "unset" database ID in resource names. */
+NSString *const kDefaultDatabaseID = @"(default)";
+
+#pragma mark - FSTDatabaseID
+
+@implementation FSTDatabaseID
+
++ (instancetype)databaseIDWithProject:(NSString *)projectID database:(NSString *)databaseID {
+ return [[FSTDatabaseID alloc] initWithProject:projectID database:databaseID];
+}
+
+/**
+ * Designated initializer.
+ *
+ * @param projectID The project for the database.
+ * @param databaseID The database in the datastore.
+ */
+- (instancetype)initWithProject:(NSString *)projectID database:(NSString *)databaseID {
+ if (self = [super init]) {
+ FSTAssert(databaseID, @"databaseID cannot be nil");
+ _projectID = [projectID copy];
+ _databaseID = [databaseID copy];
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (!other || ![[other class] isEqual:[self class]]) return NO;
+
+ return [self isEqualToDatabaseId:other];
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = [self.projectID hash];
+ hash = hash * 31u + [self.databaseID hash];
+ return hash;
+}
+
+- (NSString *)description {
+ return [NSString
+ stringWithFormat:@"<FSTDatabaseID: project:%@ database:%@>", self.projectID, self.databaseID];
+}
+
+- (NSComparisonResult)compare:(FSTDatabaseID *)other {
+ NSComparisonResult cmp = [self.projectID compare:other.projectID];
+ return cmp == NSOrderedSame ? [self.databaseID compare:other.databaseID] : cmp;
+}
+
+- (BOOL)isDefaultDatabase {
+ return [self.databaseID isEqualToString:kDefaultDatabaseID];
+}
+
+- (BOOL)isEqualToDatabaseId:(FSTDatabaseID *)databaseID {
+ if (self == databaseID) return YES;
+ if (databaseID == nil) return NO;
+ if (self.projectID != databaseID.projectID &&
+ ![self.projectID isEqualToString:databaseID.projectID])
+ return NO;
+ if (self.databaseID != databaseID.databaseID &&
+ ![self.databaseID isEqualToString:databaseID.databaseID])
+ return NO;
+ return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocument.h b/Firestore/Source/Model/FSTDocument.h
new file mode 100644
index 0000000..100f553
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocument.h
@@ -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 <Foundation/Foundation.h>
+
+@class FSTDocumentKey;
+@class FSTFieldPath;
+@class FSTFieldValue;
+@class FSTObjectValue;
+@class FSTSnapshotVersion;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The result of a lookup for a given path may be an existing document or a tombstone that marks
+ * the path deleted.
+ */
+@interface FSTMaybeDocument : NSObject <NSCopying>
+- (id)init __attribute__((unavailable("Abstract base class")));
+
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+@property(nonatomic, readonly) FSTSnapshotVersion *version;
+@end
+
+@interface FSTDocument : FSTMaybeDocument
++ (instancetype)documentWithData:(FSTObjectValue *)data
+ key:(FSTDocumentKey *)key
+ version:(FSTSnapshotVersion *)version
+ hasLocalMutations:(BOOL)mutations;
+
+- (nullable FSTFieldValue *)fieldForPath:(FSTFieldPath *)path;
+
+@property(nonatomic, strong, readonly) FSTObjectValue *data;
+@property(nonatomic, readonly, getter=hasLocalMutations) BOOL localMutations;
+
+@end
+
+@interface FSTDeletedDocument : FSTMaybeDocument
++ (instancetype)documentWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version;
+@end
+
+/** An NSComparator suitable for comparing docs using only their keys. */
+extern const NSComparator FSTDocumentComparatorByKey;
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocument.m b/Firestore/Source/Model/FSTDocument.m
new file mode 100644
index 0000000..5146d46
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocument.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 "FSTDocument.h"
+
+#import "FSTAssert.h"
+#import "FSTDocumentKey.h"
+#import "FSTFieldValue.h"
+#import "FSTPath.h"
+#import "FSTSnapshotVersion.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMaybeDocument ()
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ version:(FSTSnapshotVersion *)version NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FSTMaybeDocument
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version {
+ FSTAssert(!!version, @"Version must not be nil.");
+ self = [super init];
+ if (self) {
+ _key = key;
+ _version = version;
+ }
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *_Nullable)zone {
+ // All document types are immutable
+ return self;
+}
+
+@end
+
+@implementation FSTDocument
+
++ (instancetype)documentWithData:(FSTObjectValue *)data
+ key:(FSTDocumentKey *)key
+ version:(FSTSnapshotVersion *)version
+ hasLocalMutations:(BOOL)mutations {
+ return
+ [[FSTDocument alloc] initWithData:data key:key version:version hasLocalMutations:mutations];
+}
+
+- (instancetype)initWithData:(FSTObjectValue *)data
+ key:(FSTDocumentKey *)key
+ version:(FSTSnapshotVersion *)version
+ hasLocalMutations:(BOOL)mutations {
+ self = [super initWithKey:key version:version];
+ if (self) {
+ _data = data;
+ _localMutations = mutations;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTDocument class]]) {
+ return NO;
+ }
+
+ FSTDocument *otherDoc = other;
+ return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version] &&
+ [self.data isEqual:otherDoc.data] && self.hasLocalMutations == otherDoc.hasLocalMutations;
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.key hash];
+ result = result * 31 + [self.version hash];
+ result = result * 31 + [self.data hash];
+ result = result * 31 + (self.hasLocalMutations ? 1 : 0);
+ return result;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTDocument: key:%@ version:%@ localMutations:%@ data:%@>",
+ self.key.path, self.version,
+ self.localMutations ? @"YES" : @"NO", self.data];
+}
+
+- (nullable FSTFieldValue *)fieldForPath:(FSTFieldPath *)path {
+ return [_data valueForPath:path];
+}
+
+@end
+
+@implementation FSTDeletedDocument
+
++ (instancetype)documentWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version {
+ return [[FSTDeletedDocument alloc] initWithKey:key version:version];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTDeletedDocument class]]) {
+ return NO;
+ }
+
+ FSTDocument *otherDoc = other;
+ return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version];
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.key hash];
+ result = result * 31 + [self.version hash];
+ return result;
+}
+
+@end
+
+const NSComparator FSTDocumentComparatorByKey =
+ ^NSComparisonResult(FSTMaybeDocument *doc1, FSTMaybeDocument *doc2) {
+ return [doc1.key compare:doc2.key];
+ };
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentDictionary.h b/Firestore/Source/Model/FSTDocumentDictionary.h
new file mode 100644
index 0000000..8ae8e01
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentDictionary.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>
+
+#import "FSTImmutableSortedDictionary.h"
+
+@class FSTDocument;
+@class FSTDocumentKey;
+@class FSTMaybeDocument;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Convenience type for a map of keys to MaybeDocuments, since they are so common. */
+typedef FSTImmutableSortedDictionary<FSTDocumentKey *, FSTMaybeDocument *>
+ FSTMaybeDocumentDictionary;
+
+/** Convenience type for a map of keys to Documents, since they are so common. */
+typedef FSTImmutableSortedDictionary<FSTDocumentKey *, FSTDocument *> FSTDocumentDictionary;
+
+@interface FSTImmutableSortedDictionary (FSTDocumentDictionary)
+
+/** Returns a new set using the DocumentKeyComparator. */
++ (FSTMaybeDocumentDictionary *)maybeDocumentDictionary;
+
+/** Returns a new set using the DocumentKeyComparator. */
++ (FSTDocumentDictionary *)documentDictionary;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentDictionary.m b/Firestore/Source/Model/FSTDocumentDictionary.m
new file mode 100644
index 0000000..67e3ae7
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentDictionary.m
@@ -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 "FSTDocumentDictionary.h"
+
+#import "FSTDocumentKey.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTImmutableSortedDictionary (FSTMaybeDocumentDictionary)
+
++ (instancetype)maybeDocumentDictionary {
+ // Immutable dictionaries are contravariant in their value type, so just return a
+ // FSTDocumentDictionary here.
+ return [FSTDocumentDictionary documentDictionary];
+}
+
++ (instancetype)documentDictionary {
+ static FSTDocumentDictionary *singleton;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ singleton = [FSTDocumentDictionary dictionaryWithComparator:FSTDocumentKeyComparator];
+ });
+ return singleton;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentKey.h b/Firestore/Source/Model/FSTDocumentKey.h
new file mode 100644
index 0000000..2af1c9a
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentKey.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>
+
+@class FSTResourcePath;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** FSTDocumentKey represents the location of a document in the Firestore database. */
+@interface FSTDocumentKey : NSObject <NSCopying>
+
+/**
+ * Creates and returns a new document key with the given path.
+ *
+ * @param path The path to the document.
+ * @return A new instance of FSTDocumentKey.
+ */
++ (instancetype)keyWithPath:(FSTResourcePath *)path;
+
+/**
+ * Creates and returns a new document key with a path with the given segments.
+ *
+ * @param segments The segments of the path to the document.
+ * @return A new instance of FSTDocumentKey.
+ */
++ (instancetype)keyWithSegments:(NSArray<NSString *> *)segments;
+
+/**
+ * Creates and returns a new document key from the given resource path string.
+ *
+ * @param resourcePath The slash-separated segments of the resource's path.
+ * @return A new instance of FSTDocumentKey.
+ */
++ (instancetype)keyWithPathString:(NSString *)resourcePath;
+
+/** Returns true iff the given path is a path to a document. */
++ (BOOL)isDocumentKey:(FSTResourcePath *)path;
+
+- (BOOL)isEqualToKey:(FSTDocumentKey *)other;
+- (NSComparisonResult)compare:(FSTDocumentKey *)other;
+
+/** The path to the document. */
+@property(strong, nonatomic, readonly) FSTResourcePath *path;
+
+@end
+
+extern const NSComparator FSTDocumentKeyComparator;
+
+/** The field path string that represents the document's key. */
+extern NSString *const kDocumentKeyPath;
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentKey.m b/Firestore/Source/Model/FSTDocumentKey.m
new file mode 100644
index 0000000..a412b13
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentKey.m
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTDocumentKey.h"
+
+#import "FSTAssert.h"
+#import "FSTFirestoreClient.h"
+#import "FSTPath.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDocumentKey ()
+/** The path to the document. */
+@property(strong, nonatomic, readwrite) FSTResourcePath *path;
+@end
+
+@implementation FSTDocumentKey
+
++ (instancetype)keyWithPath:(FSTResourcePath *)path {
+ return [[FSTDocumentKey alloc] initWithPath:path];
+}
+
++ (instancetype)keyWithSegments:(NSArray<NSString *> *)segments {
+ return [FSTDocumentKey keyWithPath:[FSTResourcePath pathWithSegments:segments]];
+}
+
++ (instancetype)keyWithPathString:(NSString *)resourcePath {
+ NSArray<NSString *> *segments = [resourcePath componentsSeparatedByString:@"/"];
+ return [FSTDocumentKey keyWithSegments:segments];
+}
+
+/** Designated initializer. */
+- (instancetype)initWithPath:(FSTResourcePath *)path {
+ FSTAssert([FSTDocumentKey isDocumentKey:path], @"invalid document key path: %@", path);
+
+ if (self = [super init]) {
+ _path = path;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+ if (![object isKindOfClass:[FSTDocumentKey class]]) {
+ return NO;
+ }
+ return [self isEqualToKey:(FSTDocumentKey *)object];
+}
+
+- (NSUInteger)hash {
+ return self.path.hash;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTDocumentKey: %@>", self.path];
+}
+
+/** Implements NSCopying without actually copying because FSTDocumentKeys are immutable. */
+- (id)copyWithZone:(NSZone *_Nullable)zone {
+ return self;
+}
+
+- (BOOL)isEqualToKey:(FSTDocumentKey *)other {
+ return FSTDocumentKeyComparator(self, other) == NSOrderedSame;
+}
+
+- (NSComparisonResult)compare:(FSTDocumentKey *)other {
+ return FSTDocumentKeyComparator(self, other);
+}
+
++ (NSComparator)comparator {
+ return ^NSComparisonResult(id obj1, id obj2) {
+ return [obj1 compare:obj2];
+ };
+}
+
++ (BOOL)isDocumentKey:(FSTResourcePath *)path {
+ return path.length % 2 == 0;
+}
+
+@end
+
+const NSComparator FSTDocumentKeyComparator =
+ ^NSComparisonResult(FSTDocumentKey *key1, FSTDocumentKey *key2) {
+ return [key1.path compare:key2.path];
+ };
+
+NSString *const kDocumentKeyPath = @"__name__";
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentKeySet.h b/Firestore/Source/Model/FSTDocumentKeySet.h
new file mode 100644
index 0000000..7352985
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentKeySet.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 "FSTImmutableSortedSet.h"
+
+@class FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Convenience type for a set of keys, since they are so common. */
+typedef FSTImmutableSortedSet<FSTDocumentKey *> FSTDocumentKeySet;
+
+@interface FSTImmutableSortedSet (FSTDocumentKey)
+
+/** Returns a new set using the DocumentKeyComparator. */
++ (FSTDocumentKeySet *)keySet;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentKeySet.m b/Firestore/Source/Model/FSTDocumentKeySet.m
new file mode 100644
index 0000000..54f1b2c
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentKeySet.m
@@ -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 "FSTDocumentKeySet.h"
+
+#import "FSTDocumentKey.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTImmutableSortedSet (FSTDocumentKey)
+
++ (instancetype)keySet {
+ return [FSTDocumentKeySet setWithComparator:FSTDocumentKeyComparator];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentSet.h b/Firestore/Source/Model/FSTDocumentSet.h
new file mode 100644
index 0000000..7457ea3
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentSet.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTDocumentDictionary.h"
+
+@class FSTDocument;
+@class FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * DocumentSet is an immutable (copy-on-write) collection that holds documents in order specified
+ * by the provided comparator. We always add a document key comparator on top of what is provided
+ * to guarantee document equality based on the key.
+ */
+@interface FSTDocumentSet : NSObject
+
+/** Creates a new, empty FSTDocumentSet sorted by the given comparator, then by keys. */
++ (instancetype)documentSetWithComparator:(NSComparator)comparator;
+
+- (instancetype)init __attribute__((unavailable("Use a static constructor instead")));
+
+- (NSUInteger)count;
+
+/** Returns true if the dictionary contains no elements. */
+- (BOOL)isEmpty;
+
+/** Returns YES if this set contains a document with the given key. */
+- (BOOL)containsKey:(FSTDocumentKey *)key;
+
+/** Returns the document from this set with the given key if it exists or nil if it doesn't. */
+- (FSTDocument *_Nullable)documentForKey:(FSTDocumentKey *)key;
+
+/**
+ * Returns the first document in the set according to its built in ordering, or nil if the set
+ * is empty.
+ */
+- (FSTDocument *_Nullable)firstDocument;
+
+/**
+ * Returns the last document in the set according to its built in ordering, or nil if the set
+ * is empty.
+ */
+- (FSTDocument *_Nullable)lastDocument;
+
+/**
+ * Returns the document previous to the document associated with the given key in the set according
+ * to its built in ordering. Returns nil if the document associated with the given key is the
+ * first document.
+ *
+ * @param key A key that must be present in the DocumentSet.
+ * @throws NSInvalidArgumentException if key is not present.
+ */
+- (FSTDocument *_Nullable)predecessorDocumentForKey:(FSTDocumentKey *)key;
+
+/**
+ * Returns the index of the document with the provided key in the document set. Returns NSNotFound
+ * if the key is not present.
+ */
+- (NSUInteger)indexOfKey:(FSTDocumentKey *)key;
+
+- (NSEnumerator<FSTDocument *> *)documentEnumerator;
+
+/** Returns a copy of the documents in this set as an array. This is O(n) on the size of the set. */
+- (NSArray<FSTDocument *> *)arrayValue;
+
+/**
+ * Returns the documents as a FSTMaybeDocumentDictionary. This is O(1) as this leverages our
+ * internal representation.
+ */
+- (FSTMaybeDocumentDictionary *)dictionaryValue;
+
+/** Returns a new FSTDocumentSet that contains the given document. */
+- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document;
+
+/** Returns a new FSTDocumentSet that excludes any document associated with the given key. */
+- (instancetype)documentSetByRemovingKey:(FSTDocumentKey *)key;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentSet.m b/Firestore/Source/Model/FSTDocumentSet.m
new file mode 100644
index 0000000..94b7b58
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentSet.m
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTDocumentSet.h"
+
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTImmutableSortedSet.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The type of the index of the documents in an FSTDocumentSet.
+ * @see FSTDocumentSet#index
+ */
+typedef FSTImmutableSortedDictionary<FSTDocumentKey *, FSTDocument *> IndexType;
+
+/**
+ * The type of the main collection of documents in an FSTDocumentSet.
+ * @see FSTDocumentSet#sortedSet
+ */
+typedef FSTImmutableSortedSet<FSTDocument *> SetType;
+
+@interface FSTDocumentSet ()
+
+- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet NS_DESIGNATED_INITIALIZER;
+
+/**
+ * An index of the documents in the FSTDocumentSet, indexed by document key. The index
+ * exists to guarantee the uniqueness of document keys in the set and to allow lookup and removal
+ * of documents by key.
+ */
+@property(nonatomic, strong, readonly) IndexType *index;
+
+/**
+ * The main collection of documents in the FSTDocumentSet. The documents are ordered by a
+ * comparator supplied from a query. The SetType collection exists in addition to the index to
+ * allow ordered traversal of the FSTDocumentSet.
+ */
+@property(nonatomic, strong, readonly) SetType *sortedSet;
+@end
+
+@implementation FSTDocumentSet
+
++ (instancetype)documentSetWithComparator:(NSComparator)comparator {
+ IndexType *index =
+ [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator];
+ SetType *set = [FSTImmutableSortedSet setWithComparator:comparator];
+ return [[FSTDocumentSet alloc] initWithIndex:index set:set];
+}
+
+- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet {
+ self = [super init];
+ if (self) {
+ _index = index;
+ _sortedSet = sortedSet;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isMemberOfClass:[FSTDocumentSet class]]) {
+ return NO;
+ }
+
+ FSTDocumentSet *otherSet = (FSTDocumentSet *)other;
+ if ([self count] != [otherSet count]) {
+ return NO;
+ }
+
+ NSEnumerator<FSTDocument *> *selfIter = [self.sortedSet objectEnumerator];
+ NSEnumerator<FSTDocument *> *otherIter = [otherSet.sortedSet objectEnumerator];
+
+ FSTDocument *selfDoc = [selfIter nextObject];
+ FSTDocument *otherDoc = [otherIter nextObject];
+ while (selfDoc) {
+ if (![selfDoc isEqual:otherDoc]) {
+ return NO;
+ }
+ selfDoc = [selfIter nextObject];
+ otherDoc = [otherIter nextObject];
+ }
+ return YES;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = 0;
+ for (FSTDocument *doc in self.sortedSet.objectEnumerator) {
+ hash = 31 * hash + [doc hash];
+ }
+ return hash;
+}
+
+- (NSString *)description {
+ return [self.sortedSet description];
+}
+
+- (NSUInteger)count {
+ return [self.index count];
+}
+
+- (BOOL)isEmpty {
+ return [self.index isEmpty];
+}
+
+- (BOOL)containsKey:(FSTDocumentKey *)key {
+ return [self.index objectForKey:key] != nil;
+}
+
+- (FSTDocument *_Nullable)documentForKey:(FSTDocumentKey *)key {
+ return [self.index objectForKey:key];
+}
+
+- (FSTDocument *_Nullable)firstDocument {
+ return [self.sortedSet firstObject];
+}
+
+- (FSTDocument *_Nullable)lastDocument {
+ return [self.sortedSet lastObject];
+}
+
+- (FSTDocument *_Nullable)predecessorDocumentForKey:(FSTDocumentKey *)key {
+ FSTDocument *doc = [self.index objectForKey:key];
+ if (!doc) {
+ @throw [NSException exceptionWithName:NSInvalidArgumentException
+ reason:[NSString stringWithFormat:@"Key %@ does not exist", key]
+ userInfo:nil];
+ }
+ return [self.sortedSet predecessorObject:doc];
+}
+
+- (NSUInteger)indexOfKey:(FSTDocumentKey *)key {
+ FSTDocument *doc = [self.index objectForKey:key];
+ return doc ? [self.sortedSet indexOfObject:doc] : NSNotFound;
+}
+
+- (NSEnumerator<FSTDocument *> *)documentEnumerator {
+ return [self.sortedSet objectEnumerator];
+}
+
+- (NSArray *)arrayValue {
+ NSMutableArray<FSTDocument *> *result = [NSMutableArray arrayWithCapacity:self.count];
+ for (FSTDocument *doc in self.documentEnumerator) {
+ [result addObject:doc];
+ }
+ return result;
+}
+
+- (FSTMaybeDocumentDictionary *)dictionaryValue {
+ return self.index;
+}
+
+- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document {
+ // TODO(mcg): look into making document nonnull.
+ if (!document) {
+ return self;
+ }
+
+ // Remove any prior mapping of the document's key before adding, preventing sortedSet from
+ // accumulating values that aren't in the index.
+ FSTDocumentSet *removed = [self documentSetByRemovingKey:document.key];
+
+ IndexType *index = [removed.index dictionaryBySettingObject:document forKey:document.key];
+ SetType *set = [removed.sortedSet setByAddingObject:document];
+ return [[FSTDocumentSet alloc] initWithIndex:index set:set];
+}
+
+- (instancetype)documentSetByRemovingKey:(FSTDocumentKey *)key {
+ FSTDocument *doc = [self.index objectForKey:key];
+ if (!doc) {
+ return self;
+ }
+
+ IndexType *index = [self.index dictionaryByRemovingObjectForKey:key];
+ SetType *set = [self.sortedSet setByRemovingObject:doc];
+ return [[FSTDocumentSet alloc] initWithIndex:index set:set];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.h b/Firestore/Source/Model/FSTDocumentVersionDictionary.h
new file mode 100644
index 0000000..f94545f
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentVersionDictionary.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 "FSTImmutableSortedDictionary.h"
+
+@class FSTDocumentKey;
+@class FSTSnapshotVersion;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** A map of key to version number. */
+typedef FSTImmutableSortedDictionary<FSTDocumentKey *, FSTSnapshotVersion *>
+ FSTDocumentVersionDictionary;
+
+/**
+ * Extension to FSTImmutableSortedDictionary that allows natural construction of
+ * FSTDocumentVersionDictionary.
+ */
+@interface FSTImmutableSortedDictionary (FSTDocumentVersionDictionary)
+
++ (instancetype)documentVersionDictionary;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.m b/Firestore/Source/Model/FSTDocumentVersionDictionary.m
new file mode 100644
index 0000000..0eaf9f8
--- /dev/null
+++ b/Firestore/Source/Model/FSTDocumentVersionDictionary.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 "FSTDocumentVersionDictionary.h"
+
+#import "FSTDocumentKey.h"
+#import "FSTSnapshotVersion.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTImmutableSortedDictionary (FSTDocumentVersionDictionary)
+
++ (instancetype)documentVersionDictionary {
+ static FSTDocumentVersionDictionary *singleton;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ singleton = [FSTDocumentVersionDictionary dictionaryWithComparator:FSTDocumentKeyComparator];
+ });
+ return singleton;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTFieldValue.h b/Firestore/Source/Model/FSTFieldValue.h
new file mode 100644
index 0000000..cbf7e3e
--- /dev/null
+++ b/Firestore/Source/Model/FSTFieldValue.h
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTImmutableSortedDictionary.h"
+
+@class FSTDatabaseID;
+@class FSTDocumentKey;
+@class FSTFieldPath;
+@class FSTTimestamp;
+@class FIRGeoPoint;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** The order of types in Firestore; this order is defined by the backend. */
+typedef NS_ENUM(NSInteger, FSTTypeOrder) {
+ FSTTypeOrderNull,
+ FSTTypeOrderBoolean,
+ FSTTypeOrderNumber,
+ FSTTypeOrderTimestamp,
+ FSTTypeOrderString,
+ FSTTypeOrderBlob,
+ FSTTypeOrderReference,
+ FSTTypeOrderGeoPoint,
+ FSTTypeOrderArray,
+ FSTTypeOrderObject,
+};
+
+/**
+ * Abstract base class representing an immutable data value as stored in Firestore. FSTFieldValue
+ * represents all the different kinds of values that can be stored in fields in a document.
+ *
+ * Supported types are:
+ * - Null
+ * - Boolean
+ * - Long
+ * - Double
+ * - Timestamp
+ * - ServerTimestamp (a sentinel used in uncommitted writes)
+ * - String
+ * - Binary
+ * - (Document) References
+ * - GeoPoint
+ * - Array
+ * - Object
+ */
+@interface FSTFieldValue : NSObject
+
+/** Returns the FSTTypeOrder for this value. */
+- (FSTTypeOrder)typeOrder;
+
+/**
+ * Converts an FSTFieldValue into the value that users will see in document snapshots.
+ *
+ * TODO(mikelehen): This conversion should probably happen at the API level and right now `value` is
+ * used inappropriately in the serializer implementation, etc. We need to do some reworking.
+ */
+- (id)value;
+
+/** Compares against another FSTFieldValue. */
+- (NSComparisonResult)compare:(FSTFieldValue *)other;
+
+@end
+
+/**
+ * A null value stored in Firestore. The |value| of a FSTNullValue is [NSNull null].
+ */
+@interface FSTNullValue : FSTFieldValue
++ (instancetype)nullValue;
+- (id)value;
+@end
+
+/**
+ * A boolean value stored in Firestore.
+ */
+@interface FSTBooleanValue : FSTFieldValue
++ (instancetype)trueValue;
++ (instancetype)falseValue;
++ (instancetype)booleanValue:(BOOL)value;
+- (NSNumber *)value;
+@end
+
+/**
+ * Base class inherited from by FSTIntegerValue and FSTDoubleValue. It implements proper number
+ * comparisons between the two types.
+ */
+@interface FSTNumberValue : FSTFieldValue
+@end
+
+/**
+ * An integer value stored in Firestore.
+ */
+@interface FSTIntegerValue : FSTNumberValue
++ (instancetype)integerValue:(int64_t)value;
+- (NSNumber *)value;
+- (int64_t)internalValue;
+@end
+
+/**
+ * A double-precision floating point number stored in Firestore.
+ */
+@interface FSTDoubleValue : FSTNumberValue
++ (instancetype)doubleValue:(double)value;
++ (instancetype)nanValue;
+- (NSNumber *)value;
+- (double)internalValue;
+@end
+
+/**
+ * A string stored in Firestore.
+ */
+@interface FSTStringValue : FSTFieldValue
++ (instancetype)stringValue:(NSString *)value;
+- (NSString *)value;
+@end
+
+/**
+ * A timestamp value stored in Firestore.
+ */
+@interface FSTTimestampValue : FSTFieldValue
++ (instancetype)timestampValue:(FSTTimestamp *)value;
+- (NSDate *)value;
+- (FSTTimestamp *)internalValue;
+@end
+
+/**
+ * Represents a locally-applied Server Timestamp.
+ *
+ * Notes:
+ * - FSTServerTimestampValue instances are created as the result of applying an FSTTransformMutation
+ * (see [FSTTransformMutation applyTo]). They can only exist in the local view of a document.
+ * Therefore they do not need to be parsed or serialized.
+ * - When evaluated locally (e.g. via FSTDocumentSnapshot data), they evaluate to NSNull (at least
+ * for now, see b/62064202).
+ * - They sort after all FSTTimestampValues. With respect to other FSTServerTimestampValues, they
+ * sort by their localWriteTime.
+ */
+@interface FSTServerTimestampValue : FSTFieldValue
++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime;
+- (NSNull *)value;
+@property(nonatomic, strong, readonly) FSTTimestamp *localWriteTime;
+@end
+
+/**
+ * A geo point value stored in Firestore.
+ */
+@interface FSTGeoPointValue : FSTFieldValue
++ (instancetype)geoPointValue:(FIRGeoPoint *)value;
+- (FIRGeoPoint *)value;
+@end
+
+/**
+ * A blob value stored in Firestore.
+ */
+@interface FSTBlobValue : FSTFieldValue
++ (instancetype)blobValue:(NSData *)value;
+- (NSData *)value;
+@end
+
+/**
+ * A reference value stored in Firestore.
+ */
+@interface FSTReferenceValue : FSTFieldValue
++ (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID;
+- (FSTDocumentKey *)value;
+@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID;
+@end
+
+/**
+ * A structured object value stored in Firestore.
+ */
+@interface FSTObjectValue : FSTFieldValue
+/** Returns an empty FSTObjectValue. */
++ (instancetype)objectValue;
+
+/**
+ * Initializes this FSTObjectValue with the given dictionary.
+ */
+- (instancetype)initWithDictionary:(NSDictionary<NSString *, FSTFieldValue *> *)value;
+
+/**
+ * Initializes this FSTObjectValue with the given immutable dictionary.
+ */
+- (instancetype)initWithImmutableDictionary:
+ (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)value NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (NSDictionary<NSString *, id> *)value;
+- (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)internalValue;
+
+/** Returns the value at the given path if it exists. Returns nil otherwise. */
+- (nullable FSTFieldValue *)valueForPath:(FSTFieldPath *)fieldPath;
+
+/**
+ * Returns a new object where the field at the named path has its value set to the given value.
+ * This object remains unmodified.
+ */
+- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forPath:(FSTFieldPath *)fieldPath;
+
+/**
+ * Returns a new object where the field at the named path has been removed. If any segment of the
+ * path does not exist within this object's structure, no change is performed.
+ */
+- (FSTObjectValue *)objectByDeletingPath:(FSTFieldPath *)fieldPath;
+@end
+
+/**
+ * An array value stored in Firestore.
+ */
+@interface FSTArrayValue : FSTFieldValue
+
+/**
+ * Initializes this instance with the given array of wrapped values.
+ *
+ * @param value An immutable array of FSTFieldValue objects. Caller is responsible for copying the
+ * value or releasing all references.
+ */
+- (instancetype)initWithValueNoCopy:(NSArray<FSTFieldValue *> *)value NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (NSArray<id> *)value;
+- (NSArray<FSTFieldValue *> *)internalValue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTFieldValue.m b/Firestore/Source/Model/FSTFieldValue.m
new file mode 100644
index 0000000..7f96a3c
--- /dev/null
+++ b/Firestore/Source/Model/FSTFieldValue.m
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTFieldValue.h"
+
+#import "FIRGeoPoint+Internal.h"
+#import "FSTAssert.h"
+#import "FSTClasses.h"
+#import "FSTComparison.h"
+#import "FSTDatabaseID.h"
+#import "FSTDocumentKey.h"
+#import "FSTPath.h"
+#import "FSTTimestamp.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTFieldValue
+
+@interface FSTFieldValue ()
+- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other;
+@end
+
+@implementation FSTFieldValue
+
+- (FSTTypeOrder)typeOrder {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (id)value {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (BOOL)isEqual:(id)other {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSUInteger)hash {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSString *)description {
+ return [[self value] description];
+}
+
+- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other {
+ if (self.typeOrder > other.typeOrder) {
+ return NSOrderedDescending;
+ } else {
+ FSTAssert(self.typeOrder < other.typeOrder,
+ @"defaultCompare should not be used for values of same type.");
+ return NSOrderedAscending;
+ }
+}
+
+@end
+
+#pragma mark - FSTNullValue
+
+@implementation FSTNullValue
+
++ (instancetype)nullValue {
+ static FSTNullValue *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FSTNullValue alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderNull;
+}
+
+- (id)value {
+ return [NSNull null];
+}
+
+- (BOOL)isEqual:(id)other {
+ return [other isKindOfClass:[self class]];
+}
+
+- (NSUInteger)hash {
+ return 47;
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[self class]]) {
+ return NSOrderedSame;
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+@end
+
+#pragma mark - FSTBooleanValue
+
+@interface FSTBooleanValue ()
+@property(nonatomic, assign, readonly) BOOL internalValue;
+@end
+
+@implementation FSTBooleanValue
+
++ (instancetype)trueValue {
+ static FSTBooleanValue *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FSTBooleanValue alloc] initWithValue:YES];
+ });
+ return sharedInstance;
+}
+
++ (instancetype)falseValue {
+ static FSTBooleanValue *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FSTBooleanValue alloc] initWithValue:NO];
+ });
+ return sharedInstance;
+}
+
++ (instancetype)booleanValue:(BOOL)value {
+ return value ? [FSTBooleanValue trueValue] : [FSTBooleanValue falseValue];
+}
+
+- (id)initWithValue:(BOOL)value {
+ self = [super init];
+ if (self) {
+ _internalValue = value;
+ }
+ return self;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderBoolean;
+}
+
+- (id)value {
+ return self.internalValue ? @YES : @NO;
+}
+
+- (BOOL)isEqual:(id)other {
+ // Since we create shared instances for true / false, we can use reference equality.
+ return self == other;
+}
+
+- (NSUInteger)hash {
+ return self.internalValue ? 1231 : 1237;
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[FSTBooleanValue class]]) {
+ return FSTCompareBools(self.internalValue, ((FSTBooleanValue *)other).internalValue);
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+@end
+
+#pragma mark - FSTNumberValue
+
+@implementation FSTNumberValue
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderNumber;
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if (![other isKindOfClass:[FSTNumberValue class]]) {
+ return [self defaultCompare:other];
+ } else {
+ if ([self isKindOfClass:[FSTDoubleValue class]]) {
+ double thisDouble = ((FSTDoubleValue *)self).internalValue;
+ if ([other isKindOfClass:[FSTDoubleValue class]]) {
+ return FSTCompareDoubles(thisDouble, ((FSTDoubleValue *)other).internalValue);
+ } else {
+ FSTAssert([other isKindOfClass:[FSTIntegerValue class]], @"Unknown number value: %@",
+ other);
+ return FSTCompareMixed(thisDouble, ((FSTIntegerValue *)other).internalValue);
+ }
+ } else {
+ int64_t thisInt = ((FSTIntegerValue *)self).internalValue;
+ if ([other isKindOfClass:[FSTIntegerValue class]]) {
+ return FSTCompareInt64s(thisInt, ((FSTIntegerValue *)other).internalValue);
+ } else {
+ FSTAssert([other isKindOfClass:[FSTDoubleValue class]], @"Unknown number value: %@", other);
+ return -1 * FSTCompareMixed(((FSTDoubleValue *)other).internalValue, thisInt);
+ }
+ }
+ }
+}
+
+@end
+
+#pragma mark - FSTIntegerValue
+
+@interface FSTIntegerValue ()
+@property(nonatomic, assign, readonly) int64_t internalValue;
+@end
+
+@implementation FSTIntegerValue
+
++ (instancetype)integerValue:(int64_t)value {
+ return [[FSTIntegerValue alloc] initWithValue:value];
+}
+
+- (id)initWithValue:(int64_t)value {
+ self = [super init];
+ if (self) {
+ _internalValue = value;
+ }
+ return self;
+}
+
+- (id)value {
+ return @(self.internalValue);
+}
+
+- (BOOL)isEqual:(id)other {
+ // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them
+ // equal via isEqual:
+ return [other isKindOfClass:[FSTIntegerValue class]] &&
+ self.internalValue == ((FSTIntegerValue *)other).internalValue;
+}
+
+- (NSUInteger)hash {
+ return (((NSUInteger)self.internalValue) ^ (NSUInteger)(self.internalValue >> 32));
+}
+
+// NOTE: compare: is implemented in NumberValue.
+
+@end
+
+#pragma mark - FSTDoubleValue
+
+@interface FSTDoubleValue ()
+@property(nonatomic, assign, readonly) double internalValue;
+@end
+
+@implementation FSTDoubleValue
+
++ (instancetype)doubleValue:(double)value {
+ // Normalize NaNs to match the behavior on the backend (which uses Double.doubletoLongBits()).
+ if (isnan(value)) {
+ return [FSTDoubleValue nanValue];
+ }
+ return [[FSTDoubleValue alloc] initWithValue:value];
+}
+
++ (instancetype)nanValue {
+ static FSTDoubleValue *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FSTDoubleValue alloc] initWithValue:NAN];
+ });
+ return sharedInstance;
+}
+
+- (id)initWithValue:(double)value {
+ self = [super init];
+ if (self) {
+ _internalValue = value;
+ }
+ return self;
+}
+
+- (id)value {
+ return @(self.internalValue);
+}
+
+- (BOOL)isEqual:(id)other {
+ // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them
+ // equal via isEqual:
+
+ // NOTE: isEqual: should compare NaN equal to itself and -0.0 not equal to 0.0.
+
+ return [other isKindOfClass:[FSTDoubleValue class]] &&
+ FSTDoubleBitwiseEquals(self.internalValue, ((FSTDoubleValue *)other).internalValue);
+}
+
+- (NSUInteger)hash {
+ return FSTDoubleBitwiseHash(self.internalValue);
+}
+
+// NOTE: compare: is implemented in NumberValue.
+
+@end
+
+#pragma mark - FSTStringValue
+
+@interface FSTStringValue ()
+@property(nonatomic, copy, readonly) NSString *internalValue;
+@end
+
+// TODO(b/37267885): Add truncation support
+@implementation FSTStringValue
+
++ (instancetype)stringValue:(NSString *)value {
+ return [[FSTStringValue alloc] initWithValue:value];
+}
+
+- (id)initWithValue:(NSString *)value {
+ self = [super init];
+ if (self) {
+ _internalValue = [value copy];
+ }
+ return self;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderString;
+}
+
+- (id)value {
+ return self.internalValue;
+}
+
+- (BOOL)isEqual:(id)other {
+ return [other isKindOfClass:[FSTStringValue class]] &&
+ [self.internalValue isEqualToString:((FSTStringValue *)other).internalValue];
+}
+
+- (NSUInteger)hash {
+ return self.internalValue ? 1 : 0;
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[FSTStringValue class]]) {
+ return FSTCompareStrings(self.internalValue, ((FSTStringValue *)other).internalValue);
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+@end
+
+#pragma mark - FSTTimestampValue
+
+@interface FSTTimestampValue ()
+@property(nonatomic, strong, readonly) FSTTimestamp *internalValue;
+@end
+
+@implementation FSTTimestampValue
+
++ (instancetype)timestampValue:(FSTTimestamp *)value {
+ return [[FSTTimestampValue alloc] initWithValue:value];
+}
+
+- (id)initWithValue:(FSTTimestamp *)value {
+ self = [super init];
+ if (self) {
+ _internalValue = value; // FSTTimestamp is immutable.
+ }
+ return self;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderTimestamp;
+}
+
+- (id)value {
+ // For developers, we expose Timestamps as Dates.
+ return self.internalValue.approximateDateValue;
+}
+
+- (BOOL)isEqual:(id)other {
+ return [other isKindOfClass:[FSTTimestampValue class]] &&
+ [self.internalValue isEqual:((FSTTimestampValue *)other).internalValue];
+}
+
+- (NSUInteger)hash {
+ return [self.internalValue hash];
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[FSTTimestampValue class]]) {
+ return [self.internalValue compare:((FSTTimestampValue *)other).internalValue];
+ } else if ([other isKindOfClass:[FSTServerTimestampValue class]]) {
+ // Concrete timestamps come before server timestamps.
+ return NSOrderedAscending;
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+@end
+
+#pragma mark - FSTServerTimestampValue
+
+@implementation FSTServerTimestampValue
+
++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime {
+ return [[FSTServerTimestampValue alloc] initWithLocalWriteTime:localWriteTime];
+}
+
+- (id)initWithLocalWriteTime:(FSTTimestamp *)localWriteTime {
+ self = [super init];
+ if (self) {
+ _localWriteTime = localWriteTime;
+ }
+ return self;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderTimestamp;
+}
+
+- (NSNull *)value {
+ // For developers, server timestamps always evaluate to NSNull (for now, at least; b/62064202).
+ return [NSNull null];
+}
+
+- (BOOL)isEqual:(id)other {
+ return [other isKindOfClass:[FSTServerTimestampValue class]] &&
+ [self.localWriteTime isEqual:((FSTServerTimestampValue *)other).localWriteTime];
+}
+
+- (NSUInteger)hash {
+ return [self.localWriteTime hash];
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<ServerTimestamp localTime=%@>", self.localWriteTime];
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[FSTServerTimestampValue class]]) {
+ return [self.localWriteTime compare:((FSTServerTimestampValue *)other).localWriteTime];
+ } else if ([other isKindOfClass:[FSTTimestampValue class]]) {
+ // Server timestamps come after all concrete timestamps.
+ return NSOrderedDescending;
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+@end
+
+#pragma mark - FSTGeoPointValue
+
+@interface FSTGeoPointValue ()
+@property(nonatomic, strong, readonly) FIRGeoPoint *internalValue;
+@end
+
+@implementation FSTGeoPointValue
+
++ (instancetype)geoPointValue:(FIRGeoPoint *)value {
+ return [[FSTGeoPointValue alloc] initWithValue:value];
+}
+
+- (id)initWithValue:(FIRGeoPoint *)value {
+ self = [super init];
+ if (self) {
+ _internalValue = value; // FIRGeoPoint is immutable.
+ }
+ return self;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderGeoPoint;
+}
+
+- (id)value {
+ return self.internalValue;
+}
+
+- (BOOL)isEqual:(id)other {
+ return [other isKindOfClass:[FSTGeoPointValue class]] &&
+ [self.internalValue isEqual:((FSTGeoPointValue *)other).internalValue];
+}
+
+- (NSUInteger)hash {
+ return [self.internalValue hash];
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[FSTGeoPointValue class]]) {
+ return [self.internalValue compare:((FSTGeoPointValue *)other).internalValue];
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+@end
+
+#pragma mark - FSTBlobValue
+
+@interface FSTBlobValue ()
+@property(nonatomic, copy, readonly) NSData *internalValue;
+@end
+
+// TODO(b/37267885): Add truncation support
+@implementation FSTBlobValue
+
++ (instancetype)blobValue:(NSData *)value {
+ return [[FSTBlobValue alloc] initWithValue:value];
+}
+
+- (id)initWithValue:(NSData *)value {
+ self = [super init];
+ if (self) {
+ _internalValue = [value copy];
+ }
+ return self;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderBlob;
+}
+
+- (id)value {
+ return self.internalValue;
+}
+
+- (BOOL)isEqual:(id)other {
+ return [other isKindOfClass:[FSTBlobValue class]] &&
+ [self.internalValue isEqual:((FSTBlobValue *)other).internalValue];
+}
+
+- (NSUInteger)hash {
+ return [self.internalValue hash];
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[FSTBlobValue class]]) {
+ return FSTCompareBytes(self.internalValue, ((FSTBlobValue *)other).internalValue);
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+@end
+
+#pragma mark - FSTReferenceValue
+
+@interface FSTReferenceValue ()
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+@end
+
+@implementation FSTReferenceValue
+
++ (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID {
+ return [[FSTReferenceValue alloc] initWithValue:value databaseID:databaseID];
+}
+
+- (id)initWithValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID {
+ self = [super init];
+ if (self) {
+ _key = value;
+ _databaseID = databaseID;
+ }
+ return self;
+}
+
+- (id)value {
+ return self.key;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderReference;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTReferenceValue class]]) {
+ return NO;
+ }
+
+ FSTReferenceValue *otherRef = (FSTReferenceValue *)other;
+ return [self.key isEqualToKey:otherRef.key] &&
+ [self.databaseID isEqualToDatabaseId:otherRef.databaseID];
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.databaseID hash];
+ result = 31 * result + [self.key hash];
+ return result;
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[FSTReferenceValue class]]) {
+ FSTReferenceValue *ref = (FSTReferenceValue *)other;
+ NSComparisonResult cmp = [self.databaseID compare:ref.databaseID];
+ return cmp != NSOrderedSame ? cmp : [self.key compare:ref.key];
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+@end
+
+#pragma mark - FSTObjectValue
+
+@interface FSTObjectValue ()
+@property(nonatomic, strong, readonly)
+ FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *internalValue;
+@end
+
+@implementation FSTObjectValue
+
++ (instancetype)objectValue {
+ static FSTObjectValue *sharedEmptyInstance = nil;
+ static dispatch_once_t onceToken;
+
+ dispatch_once(&onceToken, ^{
+ FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *empty =
+ [FSTImmutableSortedDictionary dictionaryWithComparator:FSTStringComparator];
+ sharedEmptyInstance = [[FSTObjectValue alloc] initWithImmutableDictionary:empty];
+ });
+ return sharedEmptyInstance;
+}
+
+- (instancetype)initWithImmutableDictionary:
+ (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)value {
+ self = [super init];
+ if (self) {
+ _internalValue = value; // FSTImmutableSortedDictionary is immutable.
+ }
+ return self;
+}
+
+- (id)initWithDictionary:(NSDictionary<NSString *, FSTFieldValue *> *)value {
+ FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *dictionary =
+ [FSTImmutableSortedDictionary dictionaryWithDictionary:value comparator:FSTStringComparator];
+ return [self initWithImmutableDictionary:dictionary];
+}
+
+- (id)value {
+ NSMutableDictionary *result = [NSMutableDictionary dictionary];
+ [self.internalValue
+ enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) {
+ result[key] = [obj value];
+ }];
+ return result;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderObject;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTObjectValue class]]) {
+ return NO;
+ }
+
+ FSTObjectValue *otherObj = other;
+ return [self.internalValue isEqual:otherObj.internalValue];
+}
+
+- (NSUInteger)hash {
+ return [self.internalValue hash];
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[FSTObjectValue class]]) {
+ FSTImmutableSortedDictionary *selfDict = self.internalValue;
+ FSTImmutableSortedDictionary *otherDict = ((FSTObjectValue *)other).internalValue;
+ NSEnumerator *enumerator1 = [selfDict keyEnumerator];
+ NSEnumerator *enumerator2 = [otherDict keyEnumerator];
+ NSString *key1 = [enumerator1 nextObject];
+ NSString *key2 = [enumerator2 nextObject];
+ while (key1 && key2) {
+ NSComparisonResult keyCompare = [key1 compare:key2];
+ if (keyCompare != NSOrderedSame) {
+ return keyCompare;
+ }
+ NSComparisonResult valueCompare = [selfDict[key1] compare:otherDict[key2]];
+ if (valueCompare != NSOrderedSame) {
+ return valueCompare;
+ }
+ key1 = [enumerator1 nextObject];
+ key2 = [enumerator2 nextObject];
+ }
+ // Only equal if both enumerators are exhausted.
+ return FSTCompareBools(key1 != nil, key2 != nil);
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+- (nullable FSTFieldValue *)valueForPath:(FSTFieldPath *)fieldPath {
+ FSTFieldValue *value = self;
+ for (int i = 0, max = fieldPath.length; value && i < max; i++) {
+ if (![value isMemberOfClass:[FSTObjectValue class]]) {
+ return nil;
+ }
+
+ NSString *fieldName = fieldPath[i];
+ value = ((FSTObjectValue *)value).internalValue[fieldName];
+ }
+
+ return value;
+}
+
+- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forPath:(FSTFieldPath *)fieldPath {
+ FSTAssert([fieldPath length] > 0, @"Cannot set value with an empty path");
+
+ NSString *childName = [fieldPath firstSegment];
+ if ([fieldPath length] == 1) {
+ // Recursive base case:
+ return [self objectBySettingValue:value forField:childName];
+ } else {
+ // Nested path. Recursively generate a new sub-object and then wrap a new FSTObjectValue around
+ // the result.
+ FSTFieldValue *child = [_internalValue objectForKey:childName];
+ FSTObjectValue *childObject;
+ if ([child isKindOfClass:[FSTObjectValue class]]) {
+ childObject = (FSTObjectValue *)child;
+ } else {
+ // If the child is not found or is a primitive type, pretend as if an empty object lived
+ // there.
+ childObject = [FSTObjectValue objectValue];
+ }
+ FSTFieldValue *newChild =
+ [childObject objectBySettingValue:value forPath:[fieldPath pathByRemovingFirstSegment]];
+ return [self objectBySettingValue:newChild forField:childName];
+ }
+}
+
+- (FSTObjectValue *)objectByDeletingPath:(FSTFieldPath *)fieldPath {
+ FSTAssert([fieldPath length] > 0, @"Cannot delete an empty path");
+ NSString *childName = [fieldPath firstSegment];
+ if ([fieldPath length] == 1) {
+ return [[FSTObjectValue alloc]
+ initWithImmutableDictionary:[_internalValue dictionaryByRemovingObjectForKey:childName]];
+ } else {
+ FSTFieldValue *child = _internalValue[childName];
+ if ([child isKindOfClass:[FSTObjectValue class]]) {
+ FSTObjectValue *newChild =
+ [((FSTObjectValue *)child) objectByDeletingPath:[fieldPath pathByRemovingFirstSegment]];
+ return [self objectBySettingValue:newChild forField:childName];
+ } else {
+ // If the child is not found or is a primitive type, make no modifications
+ return self;
+ }
+ }
+}
+
+- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forField:(NSString *)field {
+ return [[FSTObjectValue alloc]
+ initWithImmutableDictionary:[_internalValue dictionaryBySettingObject:value forKey:field]];
+}
+
+@end
+
+@interface FSTArrayValue ()
+@property(nonatomic, strong, readonly) NSArray<FSTFieldValue *> *internalValue;
+@end
+
+#pragma mark - FSTArrayValue
+
+@implementation FSTArrayValue
+
+- (id)initWithValueNoCopy:(NSArray<FSTFieldValue *> *)value {
+ self = [super init];
+ if (self) {
+ // Does not copy, assumes the caller has already copied.
+ _internalValue = value;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[self class]]) {
+ return NO;
+ }
+
+ // NSArray's isEqual does the right thing for our purposes.
+ FSTArrayValue *otherArray = other;
+ return [self.internalValue isEqual:otherArray.internalValue];
+}
+
+- (NSUInteger)hash {
+ return [self.internalValue hash];
+}
+
+- (id)value {
+ NSMutableArray *result = [NSMutableArray arrayWithCapacity:_internalValue.count];
+ [self.internalValue enumerateObjectsUsingBlock:^(FSTFieldValue *obj, NSUInteger idx, BOOL *stop) {
+ [result addObject:[obj value]];
+ }];
+ return result;
+}
+
+- (FSTTypeOrder)typeOrder {
+ return FSTTypeOrderArray;
+}
+
+- (NSComparisonResult)compare:(FSTFieldValue *)other {
+ if ([other isKindOfClass:[FSTArrayValue class]]) {
+ NSArray<FSTFieldValue *> *selfArray = self.internalValue;
+ NSArray<FSTFieldValue *> *otherArray = ((FSTArrayValue *)other).internalValue;
+ NSUInteger minLength = MIN(selfArray.count, otherArray.count);
+ for (NSUInteger i = 0; i < minLength; i++) {
+ NSComparisonResult cmp = [selfArray[i] compare:otherArray[i]];
+ if (cmp != NSOrderedSame) {
+ return cmp;
+ }
+ }
+ return FSTCompareUIntegers(selfArray.count, otherArray.count);
+ } else {
+ return [self defaultCompare:other];
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTMutation.h b/Firestore/Source/Model/FSTMutation.h
new file mode 100644
index 0000000..ef7f1c8
--- /dev/null
+++ b/Firestore/Source/Model/FSTMutation.h
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 FSTDocument;
+@class FSTDocumentKey;
+@class FSTFieldPath;
+@class FSTFieldValue;
+@class FSTMaybeDocument;
+@class FSTObjectValue;
+@class FSTSnapshotVersion;
+@class FSTTimestamp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTFieldMask
+
+/**
+ * Provides a set of fields that can be used to partially patch a document. FieldMask is used in
+ * conjunction with ObjectValue.
+ *
+ * Examples:
+ * foo - Overwrites foo entirely with the provided value. If foo is not present in the companion
+ * ObjectValue, the field is deleted.
+ * foo.bar - Overwrites only the field bar of the object foo. If foo is not an object, foo is
+ * replaced with an object containing bar.
+ */
+@interface FSTFieldMask : NSObject
+- (id)init __attribute__((unavailable("Use initWithFields:")));
+
+/**
+ * Initializes the field mask with the given field paths. Caller is expected to either copy or
+ * or release the array of fields.
+ */
+- (instancetype)initWithFields:(NSArray<FSTFieldPath *> *)fields NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) NSArray<FSTFieldPath *> *fields;
+@end
+
+#pragma mark - FSTFieldTransform
+
+/** Represents a transform within a TransformMutation. */
+@protocol FSTTransformOperation <NSObject>
+@end
+
+/** Transforms a value into a server-generated timestamp. */
+@interface FSTServerTimestampTransform : NSObject <FSTTransformOperation>
++ (instancetype)serverTimestampTransform;
+@end
+
+/** A field path and the FSTTransformOperation to perform upon it. */
+@interface FSTFieldTransform : NSObject
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithPath:(FSTFieldPath *)path
+ transform:(id<FSTTransformOperation>)transform NS_DESIGNATED_INITIALIZER;
+@property(nonatomic, strong, readonly) FSTFieldPath *path;
+@property(nonatomic, strong, readonly) id<FSTTransformOperation> transform;
+@end
+
+#pragma mark - FSTPrecondition
+
+typedef NS_ENUM(NSUInteger, FSTPreconditionExists) {
+ FSTPreconditionExistsNotSet,
+ FSTPreconditionExistsYes,
+ FSTPreconditionExistsNo,
+};
+
+/**
+ * Encodes a precondition for a mutation. This follows the model that the backend accepts with the
+ * special case of an explicit "empty" precondition (meaning no precondition).
+ */
+@interface FSTPrecondition : NSObject
+
+/** Creates a new FSTPrecondition with an exists flag. */
++ (FSTPrecondition *)preconditionWithExists:(BOOL)exists;
+
+/** Creates a new FSTPrecondition based on a time the document exists at. */
++ (FSTPrecondition *)preconditionWithUpdateTime:(FSTSnapshotVersion *)updateTime;
+
+/** Returns a precondition representing no precondition. */
++ (FSTPrecondition *)none;
+
+/**
+ * Returns true if the preconditions is valid for the given document (or null if no document is
+ * available).
+ */
+- (BOOL)isValidForDocument:(FSTMaybeDocument *_Nullable)maybeDoc;
+
+/** Returns whether this Precondition represents no precondition. */
+- (BOOL)isNone;
+
+/** If set, preconditions a mutation based on the last updateTime. */
+@property(nonatomic, strong, readonly, nullable) FSTSnapshotVersion *updateTime;
+
+/**
+ * If set, preconditions a mutation based on whether the document exists.
+ * Uses FSTPreconditionExistsNotSet to mark as unset.
+ */
+@property(nonatomic, assign, readonly) FSTPreconditionExists exists;
+
+@end
+
+#pragma mark - FSTMutationResult
+
+@interface FSTMutationResult : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version
+ transformResults:(NSArray<FSTFieldValue *> *_Nullable)transformResults
+ NS_DESIGNATED_INITIALIZER;
+
+/** The version at which the mutation was committed or null for a delete. */
+@property(nonatomic, strong, readonly, nullable) FSTSnapshotVersion *version;
+
+/**
+ * The resulting fields returned from the backend after a FSTTransformMutation has been committed.
+ * Contains one FieldValue for each FSTFieldTransform that was in the mutation.
+ *
+ * Will be nil if the mutation was not a FSTTransformMutation.
+ */
+@property(nonatomic, strong, readonly) NSArray<FSTFieldValue *> *_Nullable transformResults;
+
+@end
+
+#pragma mark - FSTMutation
+
+/**
+ * A mutation describes a self-contained change to a document. Mutations can create, replace,
+ * delete, and update subsets of documents.
+ *
+ * ## Subclassing Notes
+ *
+ * Subclasses of FSTMutation need to implement -applyTo:hasLocalMutations: to implement the
+ * actual the behavior of mutation as applied to some source document.
+ */
+@interface FSTMutation : NSObject
+
+- (id)init NS_UNAVAILABLE;
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Applies this mutation to the given FSTDocument, FSTDeletedDocument or nil, if we don't have
+ * information about this document. Both the input and returned documents can be nil.
+ *
+ * @param maybeDoc The document to mutate. The input document should nil if it does not currently
+ * exist.
+ * @param localWriteTime A timestamp indicating the local write time of the batch this mutation is
+ * a part of.
+ * @param mutationResult Optional result info from the backend. If omitted, it's assumed that
+ * this is merely a local (latency-compensated) application, and the resulting document will
+ * have its hasLocalMutations flag set.
+ *
+ * @return The mutated document. The returned document may be nil, but only if maybeDoc was nil
+ * and the mutation would not create a new document.
+ *
+ * NOTE: We preserve the version of the base document only in case of Set or Patch mutation to
+ * denote what version of original document we've changed. In case of DeleteMutation we always reset
+ * the version.
+ *
+ * Here's the expected transition table.
+ *
+ * MUTATION APPLIED TO RESULTS IN
+ *
+ * SetMutation Document(v3) Document(v3)
+ * SetMutation DeletedDocument(v3) Document(v0)
+ * SetMutation nil Document(v0)
+ * PatchMutation Document(v3) Document(v3)
+ * PatchMutation DeletedDocument(v3) DeletedDocument(v3)
+ * PatchMutation nil nil
+ * TransformMutation Document(v3) Document(v3)
+ * TransformMutation DeletedDocument(v3) DeletedDocument(v3)
+ * TransformMutation nil nil
+ * DeleteMutation Document(v3) DeletedDocument(v0)
+ * DeleteMutation DeletedDocument(v3) DeletedDocument(v0)
+ * DeleteMutation nil DeletedDocument(v0)
+ *
+ * Note that FSTTransformMutations don't create FSTDocuments (in the case of being applied to an
+ * FSTDeletedDocument), even though they would on the backend. This is because the client always
+ * combines the FSTTransformMutation with a FSTSetMutation or FSTPatchMutation and we only want to
+ * apply the transform if the prior mutation resulted in an FSTDocument (always true for an
+ * FSTSetMutation, but not necessarily for an FSTPatchMutation).
+ */
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ localWriteTime:(FSTTimestamp *)localWriteTime
+ mutationResult:(FSTMutationResult *_Nullable)mutationResult;
+
+/**
+ * A helper version of applyTo for applying mutations locally (without a mutation result from the
+ * backend).
+ */
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ localWriteTime:(FSTTimestamp *)localWriteTime;
+
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+
+/** The precondition for this mutation. */
+@property(nonatomic, strong, readonly) FSTPrecondition *precondition;
+
+@end
+
+#pragma mark - FSTSetMutation
+
+/**
+ * A mutation that creates or replaces the document at the given key with the object value
+ * contents.
+ */
+@interface FSTSetMutation : FSTMutation
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition NS_UNAVAILABLE;
+
+/**
+ * Initializes the set mutation.
+ *
+ * @param key Identifies the location of the document to mutate.
+ * @param value An object value that describes the contents to store at the location named by the
+ * key.
+ * @param precondition The precondition for this mutation.
+ */
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ value:(FSTObjectValue *)value
+ precondition:(FSTPrecondition *)precondition NS_DESIGNATED_INITIALIZER;
+
+/** The object value to use when setting the document. */
+@property(nonatomic, strong, readonly) FSTObjectValue *value;
+@end
+
+#pragma mark - FSTPatchMutation
+
+/**
+ * A mutation that modifies fields of the document at the given key with the given values. The
+ * values are applied through a field mask:
+ *
+ * * When a field is in both the mask and the values, the corresponding field is updated.
+ * * When a field is in neither the mask nor the values, the corresponding field is unmodified.
+ * * When a field is in the mask but not in the values, the corresponding field is deleted.
+ * * When a field is not in the mask but is in the values, the values map is ignored.
+ */
+@interface FSTPatchMutation : FSTMutation
+
+/** Returns the precondition for the given FSTPrecondition. */
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition NS_UNAVAILABLE;
+
+/**
+ * Initializes a new patch mutation with an explicit FSTFieldMask and FSTObjectValue representing
+ * the updates to perform
+ *
+ * @param key Identifies the location of the document to mutate.
+ * @param fieldMask The field mask specifying at what locations the data in value should be
+ * applied.
+ * @param value An FSTObjectValue containing the data to be written (using the paths in fieldMask
+ * to determine the locations at which it should be applied).
+ * @param precondition The precondition for this mutation.
+ */
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ fieldMask:(FSTFieldMask *)fieldMask
+ value:(FSTObjectValue *)value
+ precondition:(FSTPrecondition *)precondition NS_DESIGNATED_INITIALIZER;
+
+/** The fields and associated values to use when patching the document. */
+@property(nonatomic, strong, readonly) FSTObjectValue *value;
+
+/**
+ * A mask to apply to |value|, where only fields that are in both the fieldMask and the value
+ * will be updated.
+ */
+@property(nonatomic, strong, readonly) FSTFieldMask *fieldMask;
+
+@end
+
+#pragma mark - FSTTransformMutation
+
+/**
+ * A mutation that modifies specific fields of the document with transform operations. Currently
+ * the only supported transform is a server timestamp, but IP Address, increment(n), etc. could
+ * be supported in the future.
+ *
+ * It is somewhat similar to an FSTPatchMutation in that it patches specific fields and has no
+ * effect when applied to nil or an FSTDeletedDocument (see comment on [FSTMutation applyTo] for
+ * rationale).
+ */
+@interface FSTTransformMutation : FSTMutation
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition NS_UNAVAILABLE;
+
+/**
+ * Initializes a new transform mutation with the specified field transforms.
+ *
+ * @param key Identifies the location of the document to mutate.
+ * @param fieldTransforms A list of FSTFieldTransform objects to perform to the document.
+ */
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms
+ NS_DESIGNATED_INITIALIZER;
+
+/** The field transforms to use when transforming the document. */
+@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms;
+
+@end
+
+#pragma mark - FSTDeleteMutation
+
+@interface FSTDeleteMutation : FSTMutation
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTMutation.m b/Firestore/Source/Model/FSTMutation.m
new file mode 100644
index 0000000..9704fde
--- /dev/null
+++ b/Firestore/Source/Model/FSTMutation.m
@@ -0,0 +1,575 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTMutation.h"
+
+#import "FSTAssert.h"
+#import "FSTClasses.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTFieldValue.h"
+#import "FSTPath.h"
+#import "FSTSnapshotVersion.h"
+#import "FSTTimestamp.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTFieldMask
+
+@implementation FSTFieldMask
+
+- (instancetype)initWithFields:(NSArray<FSTFieldPath *> *)fields {
+ if (self = [super init]) {
+ _fields = fields;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTFieldMask class]]) {
+ return NO;
+ }
+
+ FSTFieldMask *otherMask = (FSTFieldMask *)other;
+ return [self.fields isEqual:otherMask.fields];
+}
+
+- (NSUInteger)hash {
+ return self.fields.hash;
+}
+@end
+
+#pragma mark - FSTServerTimestampTransform
+
+@implementation FSTServerTimestampTransform
+
++ (instancetype)serverTimestampTransform {
+ static FSTServerTimestampTransform *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[FSTServerTimestampTransform alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ return [other isKindOfClass:[FSTServerTimestampTransform class]];
+}
+
+- (NSUInteger)hash {
+ // arbitrary number since all instances are equal.
+ return 37;
+}
+
+@end
+
+#pragma mark - FSTFieldTransform
+
+@implementation FSTFieldTransform
+
+- (instancetype)initWithPath:(FSTFieldPath *)path transform:(id<FSTTransformOperation>)transform {
+ self = [super init];
+ if (self) {
+ _path = path;
+ _transform = transform;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) return YES;
+ if (!other || ![[other class] isEqual:[self class]]) return NO;
+ FSTFieldTransform *otherFieldTransform = other;
+ return [self.path isEqual:otherFieldTransform.path] &&
+ [self.transform isEqual:otherFieldTransform.transform];
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = [self.path hash];
+ hash = hash * 31 + [self.transform hash];
+ return hash;
+}
+
+@end
+
+#pragma mark - FSTPrecondition
+
+@implementation FSTPrecondition
+
++ (FSTPrecondition *)preconditionWithExists:(BOOL)exists {
+ FSTPreconditionExists existsEnum = exists ? FSTPreconditionExistsYes : FSTPreconditionExistsNo;
+ return [[FSTPrecondition alloc] initWithUpdateTime:nil exists:existsEnum];
+}
+
++ (FSTPrecondition *)preconditionWithUpdateTime:(FSTSnapshotVersion *)updateTime {
+ return [[FSTPrecondition alloc] initWithUpdateTime:updateTime exists:FSTPreconditionExistsNotSet];
+}
+
++ (FSTPrecondition *)none {
+ static dispatch_once_t onceToken;
+ static FSTPrecondition *noPrecondition;
+ dispatch_once(&onceToken, ^{
+ noPrecondition =
+ [[FSTPrecondition alloc] initWithUpdateTime:nil exists:FSTPreconditionExistsNotSet];
+ });
+ return noPrecondition;
+}
+
+- (instancetype)initWithUpdateTime:(FSTSnapshotVersion *_Nullable)updateTime
+ exists:(FSTPreconditionExists)exists {
+ if (self = [super init]) {
+ _updateTime = updateTime;
+ _exists = exists;
+ }
+ return self;
+}
+
+- (BOOL)isValidForDocument:(FSTMaybeDocument *_Nullable)maybeDoc {
+ if (self.updateTime) {
+ return
+ [maybeDoc isKindOfClass:[FSTDocument class]] && [maybeDoc.version isEqual:self.updateTime];
+ } else if (self.exists != FSTPreconditionExistsNotSet) {
+ if (self.exists == FSTPreconditionExistsYes) {
+ return [maybeDoc isKindOfClass:[FSTDocument class]];
+ } else {
+ FSTAssert(self.exists == FSTPreconditionExistsNo, @"Invalid precondition");
+ return maybeDoc == nil || [maybeDoc isKindOfClass:[FSTDeletedDocument class]];
+ }
+ } else {
+ FSTAssert(self.isNone, @"Precondition should be empty");
+ return YES;
+ }
+}
+
+- (BOOL)isNone {
+ return self.updateTime == nil && self.exists == FSTPreconditionExistsNotSet;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ }
+
+ if (![other isKindOfClass:[FSTPrecondition class]]) {
+ return NO;
+ }
+
+ FSTPrecondition *otherPrecondition = (FSTPrecondition *)other;
+ // Compare references to cover nil equality
+ return (self.updateTime == otherPrecondition.updateTime ||
+ [self.updateTime isEqual:otherPrecondition.updateTime]) &&
+ self.exists == otherPrecondition.exists;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = [self.updateTime hash];
+ hash = hash * 31 + self.exists;
+ return hash;
+}
+
+- (NSString *)description {
+ if (self.isNone) {
+ return @"<FSTPrecondition <none>>";
+ } else {
+ NSString *existsString;
+ switch (self.exists) {
+ case FSTPreconditionExistsYes:
+ existsString = @"yes";
+ break;
+ case FSTPreconditionExistsNo:
+ existsString = @"no";
+ break;
+ default:
+ existsString = @"<not-set>";
+ break;
+ }
+ return [NSString stringWithFormat:@"<FSTPrecondition updateTime=%@ exists=%@>", self.updateTime,
+ existsString];
+ }
+}
+
+@end
+
+#pragma mark - FSTMutationResult
+
+@implementation FSTMutationResult
+
+- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version
+ transformResults:(NSArray<FSTFieldValue *> *_Nullable)transformResults {
+ if (self = [super init]) {
+ _version = version;
+ _transformResults = transformResults;
+ }
+ return self;
+}
+
+@end
+
+#pragma mark - FSTMutation
+
+@implementation FSTMutation
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key precondition:(FSTPrecondition *)precondition {
+ if (self = [super init]) {
+ _key = key;
+ _precondition = precondition;
+ }
+ return self;
+}
+
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ localWriteTime:(FSTTimestamp *)localWriteTime
+ mutationResult:(FSTMutationResult *_Nullable)mutationResult {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ localWriteTime:(FSTTimestamp *)localWriteTime {
+ return [self applyTo:maybeDoc localWriteTime:localWriteTime mutationResult:nil];
+}
+
+@end
+
+#pragma mark - FSTSetMutation
+
+@implementation FSTSetMutation
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ value:(FSTObjectValue *)value
+ precondition:(FSTPrecondition *)precondition {
+ if (self = [super initWithKey:key precondition:precondition]) {
+ _value = value;
+ }
+ return self;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTSetMutation key=%@ value=%@ precondition=%@>", self.key,
+ self.value, self.precondition];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTSetMutation class]]) {
+ return NO;
+ }
+
+ FSTSetMutation *otherMutation = (FSTSetMutation *)other;
+ return [self.key isEqual:otherMutation.key] && [self.value isEqual:otherMutation.value] &&
+ [self.precondition isEqual:otherMutation.precondition];
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.key hash];
+ result = 31 * result + [self.precondition hash];
+ result = 31 * result + [self.value hash];
+ return result;
+}
+
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ localWriteTime:(FSTTimestamp *)localWriteTime
+ mutationResult:(FSTMutationResult *_Nullable)mutationResult {
+ if (mutationResult) {
+ FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTSetMutation.");
+ }
+
+ if (![self.precondition isValidForDocument:maybeDoc]) {
+ return maybeDoc;
+ }
+
+ BOOL hasLocalMutations = (mutationResult == nil);
+ if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) {
+ // If the document didn't exist before, create it.
+ return [FSTDocument documentWithData:self.value
+ key:self.key
+ version:[FSTSnapshotVersion noVersion]
+ hasLocalMutations:hasLocalMutations];
+ }
+
+ FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@",
+ [maybeDoc class]);
+ FSTDocument *doc = (FSTDocument *)maybeDoc;
+
+ FSTAssert([doc.key isEqual:self.key], @"Can only set a document with the same key");
+ return [FSTDocument documentWithData:self.value
+ key:doc.key
+ version:doc.version
+ hasLocalMutations:hasLocalMutations];
+}
+@end
+
+#pragma mark - FSTPatchMutation
+
+@implementation FSTPatchMutation
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ fieldMask:(FSTFieldMask *)fieldMask
+ value:(FSTObjectValue *)value
+ precondition:(FSTPrecondition *)precondition {
+ self = [super initWithKey:key precondition:precondition];
+ if (self) {
+ _fieldMask = fieldMask;
+ _value = value;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTPatchMutation class]]) {
+ return NO;
+ }
+
+ FSTPatchMutation *otherMutation = (FSTPatchMutation *)other;
+ return [self.key isEqual:otherMutation.key] && [self.fieldMask isEqual:otherMutation.fieldMask] &&
+ [self.value isEqual:otherMutation.value] &&
+ [self.precondition isEqual:otherMutation.precondition];
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.key hash];
+ result = 31 * result + [self.precondition hash];
+ result = 31 * result + [self.fieldMask hash];
+ result = 31 * result + [self.value hash];
+ return result;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTPatchMutation key=%@ mask=%@ value=%@ precondition=%@>",
+ self.key, self.fieldMask, self.value, self.precondition];
+}
+
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ localWriteTime:(FSTTimestamp *)localWriteTime
+ mutationResult:(FSTMutationResult *_Nullable)mutationResult {
+ if (mutationResult) {
+ FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTPatchMutation.");
+ }
+
+ if (![self.precondition isValidForDocument:maybeDoc]) {
+ return maybeDoc;
+ }
+
+ BOOL hasLocalMutations = (mutationResult == nil);
+ if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) {
+ // Precondition applied, so create the document if necessary
+ FSTDocumentKey *key = maybeDoc ? maybeDoc.key : self.key;
+ FSTSnapshotVersion *version = maybeDoc ? maybeDoc.version : [FSTSnapshotVersion noVersion];
+ maybeDoc = [FSTDocument documentWithData:[FSTObjectValue objectValue]
+ key:key
+ version:version
+ hasLocalMutations:hasLocalMutations];
+ }
+
+ FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@",
+ [maybeDoc class]);
+ FSTDocument *doc = (FSTDocument *)maybeDoc;
+
+ FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key");
+
+ FSTObjectValue *newData = [self patchObjectValue:doc.data];
+ return [FSTDocument documentWithData:newData
+ key:doc.key
+ version:doc.version
+ hasLocalMutations:hasLocalMutations];
+}
+
+- (FSTObjectValue *)patchObjectValue:(FSTObjectValue *)objectValue {
+ FSTObjectValue *result = objectValue;
+ for (FSTFieldPath *fieldPath in self.fieldMask.fields) {
+ FSTFieldValue *newValue = [self.value valueForPath:fieldPath];
+ if (newValue) {
+ result = [result objectBySettingValue:newValue forPath:fieldPath];
+ } else {
+ result = [result objectByDeletingPath:fieldPath];
+ }
+ }
+ return result;
+}
+
+@end
+
+@implementation FSTTransformMutation
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms {
+ // NOTE: We set a precondition of exists: true as a safety-check, since we always combine
+ // FSTTransformMutations with a FSTSetMutation or FSTPatchMutation which (if successful) should
+ // end up with an existing document.
+ if (self = [super initWithKey:key precondition:[FSTPrecondition preconditionWithExists:YES]]) {
+ _fieldTransforms = fieldTransforms;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTTransformMutation class]]) {
+ return NO;
+ }
+
+ FSTTransformMutation *otherMutation = (FSTTransformMutation *)other;
+ return [self.key isEqual:otherMutation.key] &&
+ [self.fieldTransforms isEqual:otherMutation.fieldTransforms] &&
+ [self.precondition isEqual:otherMutation.precondition];
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.key hash];
+ result = 31 * result + [self.precondition hash];
+ result = 31 * result + [self.fieldTransforms hash];
+ return result;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTTransformMutation key=%@ transforms=%@ precondition=%@>",
+ self.key, self.fieldTransforms, self.precondition];
+}
+
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ localWriteTime:(FSTTimestamp *)localWriteTime
+ mutationResult:(FSTMutationResult *_Nullable)mutationResult {
+ if (mutationResult) {
+ FSTAssert(mutationResult.transformResults,
+ @"Transform results missing for FSTTransformMutation.");
+ }
+
+ if (![self.precondition isValidForDocument:maybeDoc]) {
+ return maybeDoc;
+ }
+
+ // We only support transforms with precondition exists, so we can only apply it to an existing
+ // document
+ FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@",
+ [maybeDoc class]);
+ FSTDocument *doc = (FSTDocument *)maybeDoc;
+
+ FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key");
+
+ BOOL hasLocalMutations = (mutationResult == nil);
+ NSArray<FSTFieldValue *> *transformResults =
+ mutationResult ? mutationResult.transformResults
+ : [self localTransformResultsWithWriteTime:localWriteTime];
+ FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults];
+ return [FSTDocument documentWithData:newData
+ key:doc.key
+ version:doc.version
+ hasLocalMutations:hasLocalMutations];
+}
+
+/**
+ * Creates an array of "transform results" (a transform result is a field value representing the
+ * result of applying a transform) for use when applying an FSTTransformMutation locally.
+ *
+ * @param localWriteTime The local time of the transform mutation (used to generate
+ * FSTServerTimestampValues).
+ * @return The transform results array.
+ */
+- (NSArray<FSTFieldValue *> *)localTransformResultsWithWriteTime:(FSTTimestamp *)localWriteTime {
+ NSMutableArray<FSTFieldValue *> *transformResults = [NSMutableArray array];
+ for (FSTFieldTransform *fieldTransform in self.fieldTransforms) {
+ if ([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]]) {
+ [transformResults addObject:[FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:localWriteTime]];
+ } else {
+ FSTFail(@"Encountered unknown transform: %@", fieldTransform);
+ }
+ }
+ return transformResults;
+}
+
+- (FSTObjectValue *)transformObject:(FSTObjectValue *)objectValue
+ transformResults:(NSArray<FSTFieldValue *> *)transformResults {
+ FSTAssert(transformResults.count == self.fieldTransforms.count,
+ @"Transform results length mismatch.");
+
+ for (NSUInteger i = 0; i < self.fieldTransforms.count; i++) {
+ FSTFieldTransform *fieldTransform = self.fieldTransforms[i];
+ id<FSTTransformOperation> transform = fieldTransform.transform;
+ FSTFieldPath *fieldPath = fieldTransform.path;
+ if ([transform isKindOfClass:[FSTServerTimestampTransform class]]) {
+ objectValue = [objectValue objectBySettingValue:transformResults[i] forPath:fieldPath];
+ } else {
+ FSTFail(@"Encountered unknown transform: %@", transform);
+ }
+ }
+ return objectValue;
+}
+
+@end
+
+#pragma mark - FSTDeleteMutation
+
+@implementation FSTDeleteMutation
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FSTDeleteMutation class]]) {
+ return NO;
+ }
+
+ FSTDeleteMutation *otherMutation = (FSTDeleteMutation *)other;
+ return [self.key isEqual:otherMutation.key] &&
+ [self.precondition isEqual:otherMutation.precondition];
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = [self.key hash];
+ result = 31 * result + [self.precondition hash];
+ return result;
+}
+
+- (NSString *)description {
+ return [NSString
+ stringWithFormat:@"<FSTDeleteMutation key=%@ precondition=%@>", self.key, self.precondition];
+}
+
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ localWriteTime:(FSTTimestamp *)localWriteTime
+ mutationResult:(FSTMutationResult *_Nullable)mutationResult {
+ if (mutationResult) {
+ FSTAssert(!mutationResult.transformResults,
+ @"Transform results received by FSTDeleteMutation.");
+ }
+
+ if (![self.precondition isValidForDocument:maybeDoc]) {
+ return maybeDoc;
+ }
+
+ if (maybeDoc) {
+ FSTAssert([maybeDoc.key isEqual:self.key], @"Can only delete a document with the same key");
+ }
+
+ return [FSTDeletedDocument documentWithKey:self.key version:[FSTSnapshotVersion noVersion]];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTMutationBatch.h b/Firestore/Source/Model/FSTMutationBatch.h
new file mode 100644
index 0000000..cbc31b6
--- /dev/null
+++ b/Firestore/Source/Model/FSTMutationBatch.h
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTDocumentKeySet.h"
+#import "FSTDocumentVersionDictionary.h"
+#import "FSTTypes.h"
+
+@class FSTMutation;
+@class FSTTimestamp;
+@class FSTMutationResult;
+@class FSTMutationBatchResult;
+@class FSTSnapshotVersion;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A BatchID that was searched for and not found or a batch ID value known to be before all known
+ * batches.
+ *
+ * FSTBatchID values from the local store are non-negative so this value is before all batches.
+ */
+extern const FSTBatchID kFSTBatchIDUnknown;
+
+/**
+ * A batch of mutations that will be sent as one unit to the backend. Batches can be marked as a
+ * tombstone if the mutation queue does not remove them immediately. When a batch is a tombstone
+ * it has no mutations.
+ */
+@interface FSTMutationBatch : NSObject
+
+/** Initializes a mutation batch with the given batchID, localWriteTime, and mutations. */
+- (instancetype)initWithBatchID:(FSTBatchID)batchID
+ localWriteTime:(FSTTimestamp *)localWriteTime
+ mutations:(NSArray<FSTMutation *> *)mutations NS_DESIGNATED_INITIALIZER;
+
+- (id)init NS_UNAVAILABLE;
+
+/**
+ * Applies all the mutations in this FSTMutationBatch to the specified document.
+ *
+ * @param maybeDoc The document to apply mutations to.
+ * @param documentKey The key of the document to apply mutations to.
+ * @param mutationBatchResult The result of applying the MutationBatch to the backend. If omitted
+ * it's assumed that this is a local (latency-compensated) application and documents will have
+ * their hasLocalMutations flag set.
+ */
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ documentKey:(FSTDocumentKey *)documentKey
+ mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult;
+
+/**
+ * A helper version of applyTo for applying mutations locally (without a mutation batch result from
+ * the backend).
+ */
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ documentKey:(FSTDocumentKey *)documentKey;
+
+/**
+ * Returns YES if this mutation batch has already been removed from the mutation queue.
+ *
+ * Note that not all implementations of the FSTMutationQueue necessarily use tombstones as a part
+ * of their implementation and generally speaking no code outside the mutation queues should really
+ * care about this.
+ */
+- (BOOL)isTombstone;
+
+/** Converts this batch to a tombstone. */
+- (FSTMutationBatch *)toTombstone;
+
+/** Returns the set of unique keys referenced by all mutations in the batch. */
+- (FSTDocumentKeySet *)keys;
+
+@property(nonatomic, assign, readonly) FSTBatchID batchID;
+@property(nonatomic, strong, readonly) FSTTimestamp *localWriteTime;
+@property(nonatomic, strong, readonly) NSArray<FSTMutation *> *mutations;
+
+@end
+
+#pragma mark - FSTMutationBatchResult
+
+/** The result of applying a mutation batch to the backend. */
+@interface FSTMutationBatchResult : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Creates a new FSTMutationBatchResult for the given batch and results. There must be one result
+ * for each mutation in the batch. This static factory caches a document=>version mapping
+ * (as docVersions).
+ */
++ (instancetype)resultWithBatch:(FSTMutationBatch *)batch
+ commitVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)mutationResults
+ streamToken:(nullable NSData *)streamToken;
+
+@property(nonatomic, strong, readonly) FSTMutationBatch *batch;
+@property(nonatomic, strong, readonly) FSTSnapshotVersion *commitVersion;
+@property(nonatomic, strong, readonly) NSArray<FSTMutationResult *> *mutationResults;
+@property(nonatomic, strong, readonly, nullable) NSData *streamToken;
+@property(nonatomic, strong, readonly) FSTDocumentVersionDictionary *docVersions;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTMutationBatch.m b/Firestore/Source/Model/FSTMutationBatch.m
new file mode 100644
index 0000000..ed0e659
--- /dev/null
+++ b/Firestore/Source/Model/FSTMutationBatch.m
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTMutationBatch.h"
+
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTMutation.h"
+#import "FSTSnapshotVersion.h"
+#import "FSTTimestamp.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+const FSTBatchID kFSTBatchIDUnknown = -1;
+
+@implementation FSTMutationBatch
+
+- (instancetype)initWithBatchID:(FSTBatchID)batchID
+ localWriteTime:(FSTTimestamp *)localWriteTime
+ mutations:(NSArray<FSTMutation *> *)mutations {
+ self = [super init];
+ if (self) {
+ _batchID = batchID;
+ _localWriteTime = localWriteTime;
+ _mutations = mutations;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ } else if (![other isKindOfClass:[FSTMutationBatch class]]) {
+ return NO;
+ }
+
+ FSTMutationBatch *otherBatch = (FSTMutationBatch *)other;
+ return self.batchID == otherBatch.batchID &&
+ [self.localWriteTime isEqual:otherBatch.localWriteTime] &&
+ [self.mutations isEqual:otherBatch.mutations];
+}
+
+- (NSUInteger)hash {
+ NSUInteger result = (NSUInteger)self.batchID;
+ result = result * 31 + self.localWriteTime.hash;
+ result = result * 31 + self.mutations.hash;
+ return result;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTMutationBatch: id=%d, localWriteTime=%@, mutations=%@>",
+ self.batchID, self.localWriteTime, self.mutations];
+}
+
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ documentKey:(FSTDocumentKey *)documentKey
+ mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult {
+ FSTAssert(!maybeDoc || [maybeDoc.key isEqualToKey:documentKey],
+ @"applyTo: key %@ doesn't match maybeDoc key %@", documentKey, maybeDoc.key);
+ if (mutationBatchResult) {
+ FSTAssert(mutationBatchResult.mutationResults.count == self.mutations.count,
+ @"Mismatch between mutations length (%lu) and results length (%lu)",
+ (unsigned long)self.mutations.count,
+ (unsigned long)mutationBatchResult.mutationResults.count);
+ }
+
+ for (NSUInteger i = 0; i < self.mutations.count; i++) {
+ FSTMutation *mutation = self.mutations[i];
+ FSTMutationResult *_Nullable mutationResult = mutationBatchResult.mutationResults[i];
+ if ([mutation.key isEqualToKey:documentKey]) {
+ maybeDoc = [mutation applyTo:maybeDoc
+ localWriteTime:self.localWriteTime
+ mutationResult:mutationResult];
+ }
+ }
+ return maybeDoc;
+}
+
+- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc
+ documentKey:(FSTDocumentKey *)documentKey {
+ return [self applyTo:maybeDoc documentKey:documentKey mutationBatchResult:nil];
+}
+
+- (BOOL)isTombstone {
+ return self.mutations.count == 0;
+}
+
+- (FSTMutationBatch *)toTombstone {
+ return [[FSTMutationBatch alloc] initWithBatchID:self.batchID
+ localWriteTime:self.localWriteTime
+ mutations:@[]];
+}
+
+// TODO(klimt): This could use NSMutableDictionary instead.
+- (FSTDocumentKeySet *)keys {
+ FSTDocumentKeySet *set = [FSTDocumentKeySet keySet];
+ for (FSTMutation *mutation in self.mutations) {
+ set = [set setByAddingObject:mutation.key];
+ }
+ return set;
+}
+
+@end
+
+#pragma mark - FSTMutationBatchResult
+
+@interface FSTMutationBatchResult ()
+- (instancetype)initWithBatch:(FSTMutationBatch *)batch
+ commitVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)mutationResults
+ streamToken:(nullable NSData *)streamToken
+ docVersions:(FSTDocumentVersionDictionary *)docVersions NS_DESIGNATED_INITIALIZER;
+@end
+
+@implementation FSTMutationBatchResult
+
+- (instancetype)initWithBatch:(FSTMutationBatch *)batch
+ commitVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)mutationResults
+ streamToken:(nullable NSData *)streamToken
+ docVersions:(FSTDocumentVersionDictionary *)docVersions {
+ if (self = [super init]) {
+ _batch = batch;
+ _commitVersion = commitVersion;
+ _mutationResults = mutationResults;
+ _streamToken = streamToken;
+ _docVersions = docVersions;
+ }
+ return self;
+}
+
++ (instancetype)resultWithBatch:(FSTMutationBatch *)batch
+ commitVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)mutationResults
+ streamToken:(nullable NSData *)streamToken {
+ FSTAssert(batch.mutations.count == mutationResults.count,
+ @"Mutations sent %lu must equal results received %lu",
+ (unsigned long)batch.mutations.count, (unsigned long)mutationResults.count);
+
+ FSTDocumentVersionDictionary *docVersions =
+ [FSTDocumentVersionDictionary documentVersionDictionary];
+ NSArray<FSTMutation *> *mutations = batch.mutations;
+ for (NSUInteger i = 0; i < mutations.count; i++) {
+ FSTSnapshotVersion *_Nullable version = mutationResults[i].version;
+ if (!version) {
+ // deletes don't have a version, so we substitute the commitVersion
+ // of the entire batch.
+ version = commitVersion;
+ }
+
+ docVersions = [docVersions dictionaryBySettingObject:version forKey:mutations[i].key];
+ }
+
+ return [[FSTMutationBatchResult alloc] initWithBatch:batch
+ commitVersion:commitVersion
+ mutationResults:mutationResults
+ streamToken:streamToken
+ docVersions:docVersions];
+}
+
+@end
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTPath.h b/Firestore/Source/Model/FSTPath.h
new file mode 100644
index 0000000..1f63f17
--- /dev/null
+++ b/Firestore/Source/Model/FSTPath.h
@@ -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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTPath represents a path sequence in the Firestore database. It is composed of an ordered
+ * sequence of string segments.
+ *
+ * ## Subclassing Notes
+ *
+ * FSTPath itself is an abstract class that must be specialized by subclasses. Subclasses should
+ * implement constructors for common string-based representations of the path and also override
+ * -canonicalString which converts back to the canonical string-based representation of the path.
+ */
+@interface FSTPath <SelfType> : NSObject
+
+/** Returns the path segment of the given index. */
+- (NSString *)segmentAtIndex:(int)index;
+- (id)objectAtIndexedSubscript:(int)index;
+
+- (BOOL)isEqual:(id)path;
+- (NSComparisonResult)compare:(SelfType)other;
+
+/**
+ * Returns a new path whose segments are the current path's plus one more.
+ *
+ * @param segment The new segment to concatenate to the path.
+ * @return A new path with this path's segment plus the new one.
+ */
+- (instancetype)pathByAppendingSegment:(NSString *)segment;
+
+/**
+ * Returns a new path whose segments are the current path's plus another's.
+ *
+ * @param path The new path whose segments should be concatenated to the path.
+ * @return A new path with this path's segment plus the new ones.
+ */
+- (instancetype)pathByAppendingPath:(SelfType)path;
+
+/** Returns a new path whose segments are the same as this one's minus the first one. */
+- (instancetype)pathByRemovingFirstSegment;
+
+/** Returns a new path whose segments are the same as this one's minus the first `count`. */
+- (instancetype)pathByRemovingFirstSegments:(int)count;
+
+/** Returns a new path whose segments are the same as this one's minus the last one. */
+- (instancetype)pathByRemovingLastSegment;
+
+/** Convenience method for getting the first segment of this path. */
+- (NSString *)firstSegment;
+
+/** Convenience method for getting the last segment of this path. */
+- (NSString *)lastSegment;
+
+/** Returns true if this path is a prefix of the given path. */
+- (BOOL)isPrefixOfPath:(SelfType)other;
+
+/** Returns a standardized string representation of this path. */
+- (NSString *)canonicalString;
+
+/** The number of segments in the path. */
+@property(nonatomic, readonly) int length;
+
+/** True if the path is empty. */
+@property(nonatomic, readonly, getter=isEmpty) BOOL empty;
+
+@end
+
+/** A dot-separated path for navigating sub-objects within a document. */
+@class FSTFieldPath;
+
+@interface FSTFieldPath : FSTPath <FSTFieldPath *>
+
+/**
+ * Creates and returns a new path with the given segments. The array of segments is not copied, so
+ * one should not mutate the array once it is passed in here.
+ *
+ * @param segments The underlying array of segments for the path.
+ * @return A new instance of FSTPath.
+ */
++ (instancetype)pathWithSegments:(NSArray<NSString *> *)segments;
+
+/**
+ * Creates and returns a new path from the server formatted field-path string, where path segments
+ * are separated by a dot "." and optionally encoded using backticks.
+ *
+ * @param fieldPath A dot-separated string representing the path.
+ */
++ (instancetype)pathWithServerFormat:(NSString *)fieldPath;
+
+/** Returns a field path that represents a document key. */
++ (instancetype)keyFieldPath;
+
+/** Returns a field path that represents an empty path. */
++ (instancetype)emptyPath;
+
+/** Returns YES if this is the `FSTFieldPath.keyFieldPath` field path. */
+- (BOOL)isKeyFieldPath;
+
+@end
+
+/** A slash-separated path for navigating resources (documents and collections) within Firestore. */
+@class FSTResourcePath;
+
+@interface FSTResourcePath : FSTPath <FSTResourcePath *>
+
+/**
+ * Creates and returns a new path with the given segments. The array of segments is not copied, so
+ * one should not mutate the array once it is passed in here.
+ *
+ * @param segments The underlying array of segments for the path.
+ * @return A new instance of FSTPath.
+ */
++ (instancetype)pathWithSegments:(NSArray<NSString *> *)segments;
+
+/**
+ * Creates and returns a new path from the given resource-path string, where the path segments are
+ * separated by a slash "/".
+ *
+ * @param resourcePath A slash-separated string representing the path.
+ */
++ (instancetype)pathWithString:(NSString *)resourcePath;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Model/FSTPath.m b/Firestore/Source/Model/FSTPath.m
new file mode 100644
index 0000000..0588612
--- /dev/null
+++ b/Firestore/Source/Model/FSTPath.m
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTPath.h"
+
+#import "FSTAssert.h"
+#import "FSTClasses.h"
+#import "FSTDocumentKey.h"
+#import "FSTUsageValidation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTPath ()
+/** An underlying array of which a subset of elements are the segments of the path. */
+@property(strong, nonatomic) NSArray<NSString *> *segments;
+/** The index into the segments array of the first segment in this path. */
+@property int offset;
+@end
+
+@implementation FSTPath
+
+/**
+ * Designated initializer.
+ *
+ * @param segments The underlying array of segments for the path.
+ * @param offset The starting index in the underlying array for the subarray to use.
+ * @param length The length of the subarray to use.
+ */
+- (instancetype)initWithSegments:(NSArray<NSString *> *)segments
+ offset:(int)offset
+ length:(int)length {
+ FSTAssert(offset <= segments.count, @"offset %d out of range %d", offset, (int)segments.count);
+ FSTAssert(length <= segments.count - offset, @"offset %d out of range %d", offset,
+ (int)segments.count - offset);
+
+ if (self = [super init]) {
+ _segments = segments;
+ _offset = offset;
+ _length = length;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (self == object) {
+ return YES;
+ }
+ if (![object isKindOfClass:[FSTPath class]]) {
+ return NO;
+ }
+ FSTPath *path = object;
+ return [self compare:path] == NSOrderedSame;
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = 0;
+ for (int i = 0; i < self.length; ++i) {
+ hash += [self segmentAtIndex:i].hash;
+ }
+ return hash;
+}
+
+- (NSString *)description {
+ return [self canonicalString];
+}
+
+- (id)objectAtIndexedSubscript:(int)index {
+ return [self segmentAtIndex:index];
+}
+
+- (NSString *)segmentAtIndex:(int)index {
+ FSTAssert(index < self.length, @"index %d out of range", index);
+ return self.segments[self.offset + index];
+}
+
+- (NSString *)firstSegment {
+ FSTAssert(!self.isEmpty, @"Cannot call firstSegment on empty path");
+ return [self segmentAtIndex:0];
+}
+
+- (NSString *)lastSegment {
+ FSTAssert(!self.isEmpty, @"Cannot call lastSegment on empty path");
+ return [self segmentAtIndex:self.length - 1];
+}
+
+- (NSComparisonResult)compare:(FSTPath *)other {
+ int length = MIN(self.length, other.length);
+ for (int i = 0; i < length; ++i) {
+ NSString *left = [self segmentAtIndex:i];
+ NSString *right = [other segmentAtIndex:i];
+ NSComparisonResult result = [left compare:right];
+ if (result != NSOrderedSame) {
+ return result;
+ }
+ }
+ if (self.length < other.length) {
+ return NSOrderedAscending;
+ }
+ if (self.length > other.length) {
+ return NSOrderedDescending;
+ }
+ return NSOrderedSame;
+}
+
+- (instancetype)pathWithSegments:(NSArray<NSString *> *)segments
+ offset:(int)offset
+ length:(int)length {
+ return [[[self class] alloc] initWithSegments:segments offset:offset length:length];
+}
+
+- (instancetype)pathByAppendingSegment:(NSString *)segment {
+ int newLength = self.length + 1;
+ NSMutableArray<NSString *> *segments = [NSMutableArray arrayWithCapacity:newLength];
+ for (int i = 0; i < self.length; ++i) {
+ [segments addObject:self[i]];
+ }
+ [segments addObject:segment];
+ return [self pathWithSegments:segments offset:0 length:newLength];
+}
+
+- (instancetype)pathByAppendingPath:(FSTPath *)path {
+ int newLength = self.length + path.length;
+ NSMutableArray<NSString *> *segments = [NSMutableArray arrayWithCapacity:newLength];
+ for (int i = 0; i < self.length; ++i) {
+ [segments addObject:self[i]];
+ }
+ for (int i = 0; i < path.length; ++i) {
+ [segments addObject:path[i]];
+ }
+ return [self pathWithSegments:segments offset:0 length:newLength];
+}
+
+- (BOOL)isEmpty {
+ return self.length == 0;
+}
+
+- (instancetype)pathByRemovingFirstSegment {
+ FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingFirstSegment on empty path");
+ return [self pathWithSegments:self.segments offset:self.offset + 1 length:self.length - 1];
+}
+
+- (instancetype)pathByRemovingFirstSegments:(int)count {
+ FSTAssert(self.length >= count, @"pathByRemovingFirstSegments:%d on path of length %d", count,
+ self.length);
+ return
+ [self pathWithSegments:self.segments offset:self.offset + count length:self.length - count];
+}
+
+- (instancetype)pathByRemovingLastSegment {
+ FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingLastSegment on empty path");
+ return [self pathWithSegments:self.segments offset:self.offset length:self.length - 1];
+}
+
+- (BOOL)isPrefixOfPath:(FSTPath *)other {
+ if (other.length < self.length) {
+ return NO;
+ }
+ for (int i = 0; i < self.length; ++i) {
+ if (![self[i] isEqual:other[i]]) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+/** Returns a standardized string representation of this path. */
+- (NSString *)canonicalString {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+@end
+
+@implementation FSTFieldPath
++ (instancetype)pathWithSegments:(NSArray<NSString *> *)segments {
+ return [[FSTFieldPath alloc] initWithSegments:segments offset:0 length:(int)segments.count];
+}
+
++ (instancetype)pathWithServerFormat:(NSString *)fieldPath {
+ NSMutableArray<NSString *> *segments = [NSMutableArray array];
+
+ // TODO(b/37244157): Once we move to v1beta1, we should make this more strict. Right now, it
+ // allows non-identifier path components, even if they aren't escaped. Technically, this will
+ // mangle paths with backticks in them used in v1alpha1, but that's fine.
+
+ const char *source = [fieldPath UTF8String];
+ char *segment = (char *)malloc(strlen(source) + 1);
+ char *segmentEnd = segment;
+
+ // If we're inside '`' backticks, then we should ignore '.' dots.
+ BOOL inBackticks = NO;
+
+ char c;
+ do {
+ // Examine current character. This is legit even on zero-length strings because there's always
+ // a null terminator.
+ c = *source++;
+ switch (c) {
+ case '\0': // Falls through
+ case '.':
+ if (!inBackticks) {
+ // Segment is complete
+ *segmentEnd = '\0';
+ if (segment == segmentEnd) {
+ FSTThrowInvalidArgument(
+ @"Invalid field path (%@). Paths must not be empty, begin with "
+ @"'.', end with '.', or contain '..'",
+ fieldPath);
+ }
+
+ [segments addObject:[NSString stringWithUTF8String:segment]];
+ segmentEnd = segment;
+ } else {
+ // copy into the current segment
+ *segmentEnd++ = c;
+ }
+ break;
+
+ case '`':
+ if (inBackticks) {
+ inBackticks = NO;
+ } else {
+ inBackticks = YES;
+ }
+ break;
+
+ case '\\':
+ // advance to escaped character
+ c = *source++;
+ // TODO(b/37244157): Make this a user-facing exception once we finalize field escaping.
+ FSTAssert(c != '\0', @"Trailing escape characters not allowed in %@", fieldPath);
+ // Fall through
+
+ default:
+ // copy into the current segment
+ *segmentEnd++ = c;
+ break;
+ }
+ } while (c);
+
+ FSTAssert(!inBackticks, @"Unterminated ` in path %@", fieldPath);
+
+ free(segment);
+ return [FSTFieldPath pathWithSegments:segments];
+}
+
++ (instancetype)keyFieldPath {
+ static FSTFieldPath *keyFieldPath;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ keyFieldPath = [FSTFieldPath pathWithSegments:@[ kDocumentKeyPath ]];
+ });
+ return keyFieldPath;
+}
+
++ (instancetype)emptyPath {
+ static FSTFieldPath *emptyPath;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ emptyPath = [FSTFieldPath pathWithSegments:@[]];
+ });
+ return emptyPath;
+}
+
+/** Return YES if the string could be used as a segment in a field path without escaping. */
++ (BOOL)isValidIdentifier:(NSString *)segment {
+ if (segment.length == 0) {
+ return NO;
+ }
+ unichar first = [segment characterAtIndex:0];
+ if (first != '_' && (first < 'a' || first > 'z') && (first < 'A' || first > 'Z')) {
+ return NO;
+ }
+ for (int i = 1; i < segment.length; i++) {
+ unichar c = [segment characterAtIndex:i];
+ if (c != '_' && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9')) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+- (BOOL)isKeyFieldPath {
+ return [self isEqual:FSTFieldPath.keyFieldPath];
+}
+
+- (NSString *)canonicalString {
+ NSMutableString *result = [NSMutableString string];
+ for (int i = 0; i < self.length; i++) {
+ if (i > 0) {
+ [result appendString:@"."];
+ }
+
+ NSString *escaped = [self[i] stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
+ escaped = [escaped stringByReplacingOccurrencesOfString:@"`" withString:@"\\`"];
+ if (![FSTFieldPath isValidIdentifier:escaped]) {
+ escaped = [NSString stringWithFormat:@"`%@`", escaped];
+ }
+
+ [result appendString:escaped];
+ }
+ return result;
+}
+
+@end
+
+@implementation FSTResourcePath
++ (instancetype)pathWithSegments:(NSArray<NSString *> *)segments {
+ return [[FSTResourcePath alloc] initWithSegments:segments offset:0 length:(int)segments.count];
+}
+
++ (instancetype)pathWithString:(NSString *)resourcePath {
+ // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__)
+ // and just passes them through raw (they exist for legacy reasons and should not be used
+ // frequently).
+
+ if ([resourcePath rangeOfString:@"//"].location != NSNotFound) {
+ FSTThrowInvalidArgument(@"Invalid path (%@). Paths must not contain // in them.", resourcePath);
+ }
+
+ NSMutableArray *segments = [[resourcePath componentsSeparatedByString:@"/"] mutableCopy];
+ // We may still have an empty segment at the beginning or end if they had a leading or trailing
+ // slash (which we allow).
+ [segments removeObject:@""];
+
+ return [self pathWithSegments:segments];
+}
+
+- (NSString *)canonicalString {
+ // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__)
+ // and just passes them through raw (they exist for legacy reasons and should not be used
+ // frequently).
+
+ NSMutableString *result = [NSMutableString string];
+ for (int i = 0; i < self.length; i++) {
+ if (i > 0) {
+ [result appendString:@"/"];
+ }
+ [result appendString:self[i]];
+ }
+ return result;
+}
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRCollectionReference.h b/Firestore/Source/Public/FIRCollectionReference.h
new file mode 100644
index 0000000..11cb969
--- /dev/null
+++ b/Firestore/Source/Public/FIRCollectionReference.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRFirestoreSwiftNameSupport.h"
+#import "FIRQuery.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDocumentReference;
+
+/**
+ * A `FIRCollectionReference` object can be used for adding documents, getting document references,
+ * and querying for documents (using the methods inherited from `FIRQuery`).
+ */
+FIR_SWIFT_NAME(CollectionReference)
+@interface FIRCollectionReference : FIRQuery
+
+/** */
+- (id)init __attribute__((unavailable("FIRCollectionReference cannot be created directly.")));
+
+/** ID of the referenced collection. */
+@property(nonatomic, strong, readonly) NSString *collectionID;
+
+/**
+ * For subcollections, `parent` returns the containing `FIRDocumentReference`. For root
+ * collections, nil is returned.
+ */
+@property(nonatomic, strong, nullable, readonly) FIRDocumentReference *parent;
+
+/**
+ * A string containing the slash-separated path to this this `FIRCollectionReference` (relative to
+ * the root of the database).
+ */
+@property(nonatomic, strong, readonly) NSString *path;
+
+/**
+ * Returns a FIRDocumentReference pointing to a new document with an auto-generated ID.
+ *
+ * @return A FIRDocumentReference pointing to a new document with an auto-generated ID.
+ */
+- (FIRDocumentReference *)documentWithAutoID FIR_SWIFT_NAME(document());
+
+/**
+ * Gets a `FIRDocumentReference` referring to the document at the specified path, relative to this
+ * collection's own path.
+ *
+ * @param documentPath The slash-separated relative path of the document for which to get a
+ * `FIRDocumentReference`.
+ *
+ * @return The `FIRDocumentReference` for the specified document path.
+ */
+- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath FIR_SWIFT_NAME(document(_:));
+
+/**
+ * Add a new document to this collection with the specified data, assigning it a document ID
+ * automatically.
+ *
+ * @param data An `NSDictionary` containing the data for the new document.
+ *
+ * @return A `FIRDocumentReference` pointing to the newly created document.
+ */
+- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data
+ FIR_SWIFT_NAME(addDocument(data:));
+
+/**
+ * Add a new document to this collection with the specified data, assigning it a document ID
+ * automatically.
+ *
+ * @param data An `NSDictionary` containing the data for the new document.
+ * @param completion A block to execute once the document has been successfully written.
+ *
+ * @return A `FIRDocumentReference` pointing to the newly created document.
+ */
+// clang-format off
+// clang-format breaks the FIR_SWIFT_NAME attribute
+- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data
+ completion:
+ (nullable void (^)(NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(addDocument(data:completion:));
+// clang-format on
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRDocumentChange.h b/Firestore/Source/Public/FIRDocumentChange.h
new file mode 100644
index 0000000..674e3b2
--- /dev/null
+++ b/Firestore/Source/Public/FIRDocumentChange.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDocumentSnapshot;
+
+/** An enumeration of document change types. */
+typedef NS_ENUM(NSInteger, FIRDocumentChangeType) {
+ /** Indicates a new document was added to the set of documents matching the query. */
+ FIRDocumentChangeTypeAdded,
+ /** Indicates a document within the query was modified. */
+ FIRDocumentChangeTypeModified,
+ /**
+ * Indicates a document within the query was removed (either deleted or no longer matches
+ * the query.
+ */
+ FIRDocumentChangeTypeRemoved
+} FIR_SWIFT_NAME(DocumentChangeType);
+
+/**
+ * A `FIRDocumentChange` represents a change to the documents matching a query. It contains the
+ * document affected and the type of change that occurred (added, modified, or removed).
+ */
+FIR_SWIFT_NAME(DocumentChange)
+@interface FIRDocumentChange : NSObject
+
+/** */
+- (id)init __attribute__((unavailable("FIRDocumentChange cannot be created directly.")));
+
+/** The type of change that occurred (added, modified, or removed). */
+@property(nonatomic, readonly) FIRDocumentChangeType type;
+
+/** The document affected by this change. */
+@property(nonatomic, strong, readonly) FIRDocumentSnapshot *document;
+
+/**
+ * The index of the changed document in the result set immediately prior to this FIRDocumentChange
+ * (i.e. supposing that all prior FIRDocumentChange objects have been applied). NSNotFound for
+ * FIRDocumentChangeTypeAdded events.
+ */
+@property(nonatomic, readonly) NSUInteger oldIndex;
+
+/**
+ * The index of the changed document in the result set immediately after this FIRDocumentChange
+ * (i.e. supposing that all prior FIRDocumentChange objects and the current FIRDocumentChange object
+ * have been applied). NSNotFound for FIRDocumentChangeTypeRemoved events.
+ */
+@property(nonatomic, readonly) NSUInteger newIndex;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h
new file mode 100644
index 0000000..03340c1
--- /dev/null
+++ b/Firestore/Source/Public/FIRDocumentReference.h
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FIRFirestoreSwiftNameSupport.h"
+#import "FIRListenerRegistration.h"
+
+@class FIRFirestore;
+@class FIRCollectionReference;
+@class FIRDocumentSnapshot;
+@class FIRSetOptions;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Options for use with `[FIRDocumentReference addSnapshotListener]` to control the behavior of the
+ * snapshot listener.
+ */
+FIR_SWIFT_NAME(DocumentListenOptions)
+@interface FIRDocumentListenOptions : NSObject
+
++ (instancetype)options NS_SWIFT_UNAVAILABLE("Use initializer");
+
+- (instancetype)init;
+
+@property(nonatomic, assign, readonly) BOOL includeMetadataChanges;
+
+/**
+ * Sets the includeMetadataChanges option which controls whether metadata-only changes (i.e. only
+ * `FIRDocumentSnapshot.metadata` changed) should trigger snapshot events. Default is NO.
+ *
+ * @param includeMetadataChanges Whether to raise events for metadata-only changes.
+ * @return The receiver is returned for optional method chaining.
+ */
+- (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges
+ FIR_SWIFT_NAME(includeMetadataChanges(_:));
+
+@end
+
+typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot,
+ NSError *_Nullable error);
+
+/**
+ * A `FIRDocumentReference` refers to a document location in a Firestore database and can be
+ * used to write, read, or listen to the location. The document at the referenced location
+ * may or may not exist. A `FIRDocumentReference` can also be used to create a
+ * `FIRCollectionReference` to a subcollection.
+ */
+FIR_SWIFT_NAME(DocumentReference)
+@interface FIRDocumentReference : NSObject
+
+/** */
+- (instancetype)init
+ __attribute__((unavailable("FIRDocumentReference cannot be created directly.")));
+
+/** The ID of the document referred to. */
+@property(nonatomic, strong, readonly) NSString *documentID;
+
+/** A reference to the collection to which this `DocumentReference` belongs. */
+@property(nonatomic, strong, readonly) FIRCollectionReference *parent;
+
+/** The `FIRFirestore` for the Firestore database (useful for performing transactions, etc.). */
+@property(nonatomic, strong, readonly) FIRFirestore *firestore;
+
+/**
+ * A string representing the path of the referenced document (relative to the root of the
+ * database).
+ */
+@property(nonatomic, strong, readonly) NSString *path;
+
+/**
+ * Gets a `FIRCollectionReference` referring to the collection at the specified
+ * path, relative to this document.
+ *
+ * @param collectionPath The slash-separated relative path of the collection for which to get a
+ * `FIRCollectionReference`.
+ *
+ * @return The `FIRCollectionReference` at the specified _collectionPath_.
+ */
+- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath
+ FIR_SWIFT_NAME(collection(_:));
+
+#pragma mark - Writing Data
+
+/**
+ * Writes to the document referred to by `FIRDocumentReference`. If the document doesn't yet exist,
+ * this method creates it and then sets the data. If the document exists, this method overwrites
+ * the document data with the new values.
+ *
+ * @param documentData An `NSDictionary` that contains the fields and data to write to the
+ * document.
+ */
+- (void)setData:(NSDictionary<NSString *, id> *)documentData;
+
+/**
+ * Writes to the document referred to by this DocumentReference. If the document does not yet
+ * exist, it will be created. If you pass `FIRSetOptions`, the provided data will be merged into
+ * an existing document.
+ *
+ * @param documentData An `NSDictionary` that contains the fields and data to write to the
+ * document.
+ * @param options A `FIRSetOptions` used to configure the set behavior.
+ */
+- (void)setData:(NSDictionary<NSString *, id> *)documentData options:(FIRSetOptions *)options;
+
+/**
+ * Overwrites the document referred to by this `FIRDocumentReference`. If no document exists, it
+ * is created. If a document already exists, it is overwritten.
+ *
+ * @param documentData An `NSDictionary` containing the fields that make up the document
+ * to be written.
+ * @param completion A block to execute once the document has been successfully written.
+ */
+- (void)setData:(NSDictionary<NSString *, id> *)documentData
+ completion:(nullable void (^)(NSError *_Nullable error))completion;
+
+/**
+ * Writes to the document referred to by this DocumentReference. If the document does not yet
+ * exist, it will be created. If you pass `FIRSetOptions`, the provided data will be merged into
+ * an existing document.
+ *
+ * @param documentData An `NSDictionary` containing the fields that make up the document
+ * to be written.
+ * @param options A `FIRSetOptions` used to configure the set behavior.
+ * @param completion A block to execute once the document has been successfully written.
+ */
+- (void)setData:(NSDictionary<NSString *, id> *)documentData
+ options:(FIRSetOptions *)options
+ completion:(nullable void (^)(NSError *_Nullable error))completion;
+
+/**
+ * Updates fields in the document referred to by this `FIRDocumentReference`.
+ * If the document does not exist, the update fails (specify a completion block to be notified).
+ *
+ * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or
+ * `FIRFieldPath`) and values with which to update the document.
+ */
+- (void)updateData:(NSDictionary<id, id> *)fields;
+
+/**
+ * Updates fields in the document referred to by this `FIRDocumentReference`. If the document
+ * does not exist, the update fails and the specified completion block receives an error.
+ *
+ * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or
+ * `FIRFieldPath`) and values with which to update the document.
+ * @param completion A block to execute when the update is complete. If the update is successful the
+ * error parameter will be nil, otherwise it will give an indication of how the update failed.
+ */
+- (void)updateData:(NSDictionary<id, id> *)fields
+ completion:(nullable void (^)(NSError *_Nullable error))completion;
+
+// NOTE: this is named 'deleteDocument' because 'delete' is a keyword in Objective-C++.
+/** Deletes the document referred to by this `FIRDocumentReference`. */
+// clang-format off
+- (void)deleteDocument FIR_SWIFT_NAME(delete());
+// clang-format on
+
+/**
+ * Deletes the document referred to by this `FIRDocumentReference`.
+ *
+ * @param completion A block to execute once the document has been successfully deleted.
+ */
+// clang-format off
+- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(delete(completion:));
+// clang-format on
+
+#pragma mark - Retrieving Data
+
+/**
+ * Reads the document referenced by this `FIRDocumentReference`.
+ *
+ * @param completion a block to execute once the document has been successfully read.
+ */
+- (void)getDocumentWithCompletion:(FIRDocumentSnapshotBlock)completion
+ FIR_SWIFT_NAME(getDocument(completion:));
+
+/**
+ * Attaches a listener for DocumentSnapshot events.
+ *
+ * @param listener The listener to attach.
+ *
+ * @return A FIRListenerRegistration that can be used to remove this listener.
+ */
+- (id<FIRListenerRegistration>)addSnapshotListener:(FIRDocumentSnapshotBlock)listener
+ FIR_SWIFT_NAME(addSnapshotListener(_:));
+
+/**
+ * Attaches a listener for DocumentSnapshot events.
+ *
+ * @param options Options controlling the listener behavior.
+ * @param listener The listener to attach.
+ *
+ * @return A FIRListenerRegistration that can be used to remove this listener.
+ */
+// clang-format off
+- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions:
+ (nullable FIRDocumentListenOptions *)options
+ listener:(FIRDocumentSnapshotBlock)listener
+ FIR_SWIFT_NAME(addSnapshotListener(options:listener:));
+// clang-format on
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h
new file mode 100644
index 0000000..e923e3e
--- /dev/null
+++ b/Firestore/Source/Public/FIRDocumentSnapshot.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FIRFirestoreSwiftNameSupport.h"
+
+@class FIRDocumentReference;
+@class FIRSnapshotMetadata;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A `FIRDocumentSnapshot` contains data read from a document in your Firestore database. The data
+ * can be extracted with the `data` property or by using subscript syntax to access a specific
+ * field.
+ */
+FIR_SWIFT_NAME(DocumentSnapshot)
+@interface FIRDocumentSnapshot : NSObject
+
+/** */
+- (instancetype)init
+ __attribute__((unavailable("FIRDocumentSnapshot cannot be created directly.")));
+
+/** True if the document exists. */
+@property(nonatomic, assign, readonly) BOOL exists;
+
+/** A `FIRDocumentReference` to the document location. */
+@property(nonatomic, strong, readonly) FIRDocumentReference *reference;
+
+/** The ID of the document for which this `FIRDocumentSnapshot` contains data. */
+@property(nonatomic, copy, readonly) NSString *documentID;
+
+/** Metadata about this snapshot concerning its source and if it has local modifications. */
+@property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata;
+
+/**
+ * Retrieves all fields in the document as an `NSDictionary`.
+ *
+ * @return An `NSDictionary` containing all fields in the document.
+ */
+- (NSDictionary<NSString *, id> *)data;
+
+/**
+ * Retrieves a specific field from the document.
+ *
+ * @param key The field to retrieve.
+ *
+ * @return The value contained in the field or `nil` if the field doesn't exist.
+ */
+- (nullable id)objectForKeyedSubscript:(id)key;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRFieldPath.h b/Firestore/Source/Public/FIRFieldPath.h
new file mode 100644
index 0000000..b80eda7
--- /dev/null
+++ b/Firestore/Source/Public/FIRFieldPath.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>
+
+#import "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A `FieldPath` refers to a field in a document. The path may consist of a single field name
+ * (referring to a top level field in the document), or a list of field names (referring to a nested
+ * field in the document).
+ */
+FIR_SWIFT_NAME(FieldPath)
+@interface FIRFieldPath : NSObject <NSCopying>
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Creates a `FieldPath` from the provided field names. If more than one field name is provided, the
+ * path will point to a nested field in a document.
+ *
+ * @param fieldNames A list of field names.
+ * @return A `FieldPath` that points to a field location in a document.
+ */
+- (instancetype)initWithFields:(NSArray<NSString *> *)fieldNames FIR_SWIFT_NAME(init(_:));
+
+/**
+ * A special sentinel `FieldPath` to refer to the ID of a document. It can be used in queries to
+ * sort or filter by the document ID.
+ */
++ (instancetype)documentID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRFieldValue.h b/Firestore/Source/Public/FIRFieldValue.h
new file mode 100644
index 0000000..f7d19f0
--- /dev/null
+++ b/Firestore/Source/Public/FIRFieldValue.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 "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Sentinel values that can be used when writing document fields with setData() or updateData().
+ */
+FIR_SWIFT_NAME(FieldValue)
+@interface FIRFieldValue : NSObject
+
+/** */
+- (instancetype)init NS_UNAVAILABLE;
+
+/** Used with updateData() to mark a field for deletion. */
+// clang-format off
++ (instancetype)fieldValueForDelete FIR_SWIFT_NAME(delete());
+// clang-format on
+
+/**
+ * Used with setData() or updateData() to include a server-generated timestamp in the written
+ * data.
+ */
++ (instancetype)fieldValueForServerTimestamp FIR_SWIFT_NAME(serverTimestamp());
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRFirestore.h b/Firestore/Source/Public/FIRFirestore.h
new file mode 100644
index 0000000..c31fef6
--- /dev/null
+++ b/Firestore/Source/Public/FIRFirestore.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRFirestoreSwiftNameSupport.h"
+
+@class FIRApp;
+@class FIRCollectionReference;
+@class FIRDocumentReference;
+@class FIRFirestoreSettings;
+@class FIRTransaction;
+@class FIRWriteBatch;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * `FIRFirestore` represents a Firestore Database and is the entry point for all Firestore
+ * operations.
+ */
+FIR_SWIFT_NAME(Firestore)
+@interface FIRFirestore : NSObject
+
+#pragma mark - Initializing
+/** */
+- (instancetype)init __attribute__((unavailable("Use a static constructor method.")));
+
+/**
+ * Creates, caches, and returns a `FIRFirestore` using the default `FIRApp`. Each subsequent
+ * invocation returns the same `FIRFirestore` object.
+ *
+ * @return The `FIRFirestore` instance.
+ */
++ (instancetype)firestore FIR_SWIFT_NAME(firestore());
+
+/**
+ * Creates, caches, and returns a `FIRFirestore` object for the specified _app_. Each subsequent
+ * invocation returns the same `FIRFirestore` object.
+ *
+ * @param app The `FIRApp` instance to use for authentication and as a source of the Google Cloud
+ * Project ID for your Firestore Database. If you want the default instance, you should explicitly
+ * set it to `[FIRApp defaultApp]`.
+ *
+ * @return The `FIRFirestore` instance.
+ */
++ (instancetype)firestoreForApp:(FIRApp *)app FIR_SWIFT_NAME(firestore(app:));
+
+/**
+ * Custom settings used to configure this `FIRFirestore` object.
+ */
+@property(nonatomic, copy) FIRFirestoreSettings *settings;
+
+/**
+ * The Firebase App associated with this Firestore instance.
+ */
+@property(strong, nonatomic, readonly) FIRApp *app;
+
+#pragma mark - Collections and Documents
+
+/**
+ * Gets a `FIRCollectionReference` referring to the collection at the specified path within the
+ * database.
+ *
+ * @param collectionPath The slash-separated path of the collection for which to get a
+ * `FIRCollectionReference`.
+ *
+ * @return The `FIRCollectionReference` at the specified _collectionPath_.
+ */
+- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath
+ FIR_SWIFT_NAME(collection(_:));
+
+/**
+ * Gets a `FIRDocumentReference` referring to the document at the specified path within the
+ * database.
+ *
+ * @param documentPath The slash-separated path of the document for which to get a
+ * `FIRDocumentReference`.
+ *
+ * @return The `FIRDocumentReference` for the specified _documentPath_.
+ */
+- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath FIR_SWIFT_NAME(document(_:));
+
+#pragma mark - Transactions and Write Batches
+
+/**
+ * Executes the given updateBlock and then attempts to commit the changes applied within an atomic
+ * transaction.
+ *
+ * In the updateBlock, a set of reads and writes can be performed atomically using the
+ * `FIRTransaction` object passed to the block. After the updateBlock is run, Firestore will attempt
+ * to apply the changes to the server. If any of the data read has been modified outside of this
+ * transaction since being read, then the transaction will be retried by executing the updateBlock
+ * again. If the transaction still fails after 5 retries, then the transaction will fail.
+ *
+ * Since the updateBlock may be executed multiple times, it should avoiding doing anything that
+ * would cause side effects.
+ *
+ * Any value maybe be returned from the updateBlock. If the transaction is successfully committed,
+ * then the completion block will be passed that value. The updateBlock also has an `NSError` out
+ * parameter. If this is set, then the transaction will not attempt to commit, and the given error
+ * will be passed to the completion block.
+ *
+ * The `FIRTransaction` object passed to the updateBlock contains methods for accessing documents
+ * and collections. Unlike other firestore access, data accessed with the transaction will not
+ * reflect local changes that have not been committed. For this reason, it is required that all
+ * reads are performed before any writes. Transactions must be performed while online. Otherwise,
+ * reads will fail, and the final commit will fail.
+ *
+ * @param updateBlock The block to execute within the transaction context.
+ * @param completion The block to call with the result or error of the transaction.
+ */
+- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock
+ completion:(void (^)(id _Nullable result, NSError *_Nullable error))completion;
+
+/**
+ * Creates a write batch, used for performing multiple writes as a single
+ * atomic operation.
+ *
+ * Unlike transactions, write batches are persisted offline and therefore are preferable when you
+ * don't need to condition your writes on read data.
+ */
+- (FIRWriteBatch *)batch;
+
+#pragma mark - Logging
+
+/** Enables or disables logging from the Firestore client. */
++ (void)enableLogging:(BOOL)logging
+ DEPRECATED_MSG_ATTRIBUTE("Use FIRSetLoggerLevel(FIRLoggerLevelDebug) to enable logging");
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRFirestoreErrors.h b/Firestore/Source/Public/FIRFirestoreErrors.h
new file mode 100644
index 0000000..f2e19d9
--- /dev/null
+++ b/Firestore/Source/Public/FIRFirestoreErrors.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** The Cloud Firestore error domain. */
+FOUNDATION_EXPORT NSString *const FIRFirestoreErrorDomain FIR_SWIFT_NAME(FirestoreErrorDomain);
+
+/** Error codes used by Cloud Firestore. */
+typedef NS_ENUM(NSInteger, FIRFirestoreErrorCode) {
+ /**
+ * The operation completed successfully. NSError objects will never have a code with this value.
+ */
+ FIRFirestoreErrorCodeOK = 0,
+
+ /** The operation was cancelled (typically by the caller). */
+ FIRFirestoreErrorCodeCancelled = 1,
+
+ /** Unknown error or an error from a different error domain. */
+ FIRFirestoreErrorCodeUnknown = 2,
+
+ /**
+ * Client specified an invalid argument. Note that this differs from FailedPrecondition.
+ * InvalidArgument indicates arguments that are problematic regardless of the state of the
+ * system (e.g., an invalid field name).
+ */
+ FIRFirestoreErrorCodeInvalidArgument = 3,
+
+ /**
+ * Deadline expired before operation could complete. For operations that change the state of the
+ * system, this error may be returned even if the operation has completed successfully. For
+ * example, a successful response from a server could have been delayed long enough for the
+ * deadline to expire.
+ */
+ FIRFirestoreErrorCodeDeadlineExceeded = 4,
+
+ /** Some requested document was not found. */
+ FIRFirestoreErrorCodeNotFound = 5,
+
+ /** Some document that we attempted to create already exists. */
+ FIRFirestoreErrorCodeAlreadyExists = 6,
+
+ /** The caller does not have permission to execute the specified operation. */
+ FIRFirestoreErrorCodePermissionDenied = 7,
+
+ /**
+ * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
+ * is out of space.
+ */
+ FIRFirestoreErrorCodeResourceExhausted = 8,
+
+ /**
+ * Operation was rejected because the system is not in a state required for the operation's
+ * execution.
+ */
+ FIRFirestoreErrorCodeFailedPrecondition = 9,
+
+ /**
+ * The operation was aborted, typically due to a concurrency issue like transaction aborts, etc.
+ */
+ FIRFirestoreErrorCodeAborted = 10,
+
+ /** Operation was attempted past the valid range. */
+ FIRFirestoreErrorCodeOutOfRange = 11,
+
+ /** Operation is not implemented or not supported/enabled. */
+ FIRFirestoreErrorCodeUnimplemented = 12,
+
+ /**
+ * Internal errors. Means some invariants expected by underlying system has been broken. If you
+ * see one of these errors, something is very broken.
+ */
+ FIRFirestoreErrorCodeInternal = 13,
+
+ /**
+ * The service is currently unavailable. This is a most likely a transient condition and may be
+ * corrected by retrying with a backoff.
+ */
+ FIRFirestoreErrorCodeUnavailable = 14,
+
+ /** Unrecoverable data loss or corruption. */
+ FIRFirestoreErrorCodeDataLoss = 15,
+
+ /** The request does not have valid authentication credentials for the operation. */
+ FIRFirestoreErrorCodeUnauthenticated = 16
+} FIR_SWIFT_NAME(FirestoreErrorCode);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRFirestoreSettings.h b/Firestore/Source/Public/FIRFirestoreSettings.h
new file mode 100644
index 0000000..7097e60
--- /dev/null
+++ b/Firestore/Source/Public/FIRFirestoreSettings.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>
+
+#import "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Settings used to configure a `FIRFirestore` instance. */
+FIR_SWIFT_NAME(FirestoreSettings)
+@interface FIRFirestoreSettings : NSObject <NSCopying>
+
+/**
+ * Creates and returns an empty `FIRFirestoreSettings` object.
+ *
+ * @return The created `FIRFirestoreSettings` object.
+ */
+- (instancetype)init NS_DESIGNATED_INITIALIZER;
+
+/** The hostname to connect to. */
+@property(nonatomic, copy) NSString *host;
+
+/** Whether to use SSL when connecting. */
+@property(nonatomic, getter=isSSLEnabled) BOOL sslEnabled;
+
+/**
+ * A dispatch queue to be used to execute all completion handlers and event handlers. By default,
+ * the main queue is used.
+ */
+@property(nonatomic, strong) dispatch_queue_t dispatchQueue;
+
+/** Set to false to disable local persistent storage. */
+@property(nonatomic, getter=isPersistenceEnabled) BOOL persistenceEnabled;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h b/Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h
new file mode 100644
index 0000000..216c047
--- /dev/null
+++ b/Firestore/Source/Public/FIRFirestoreSwiftNameSupport.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
diff --git a/Firestore/Source/Public/FIRGeoPoint.h b/Firestore/Source/Public/FIRGeoPoint.h
new file mode 100644
index 0000000..de409b5
--- /dev/null
+++ b/Firestore/Source/Public/FIRGeoPoint.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 "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An immutable object representing a geographical point in Firestore. The point is represented as
+ * a latitude/longitude pair.
+ *
+ * Latitude values are in the range of [-90, 90].
+ * Longitude values are in the range of [-180, 180].
+ */
+FIR_SWIFT_NAME(GeoPoint)
+@interface FIRGeoPoint : NSObject <NSCopying>
+
+/** */
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Creates a `GeoPoint` from the provided latitude and longitude degrees.
+ * @param latitude The latitude as number between -90 and 90.
+ * @param longitude The longitude as number between -180 and 180.
+ */
+- (instancetype)initWithLatitude:(double)latitude
+ longitude:(double)longitude NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, readonly) double latitude;
+@property(nonatomic, readonly) double longitude;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRListenerRegistration.h b/Firestore/Source/Public/FIRListenerRegistration.h
new file mode 100644
index 0000000..5fe7fd5
--- /dev/null
+++ b/Firestore/Source/Public/FIRListenerRegistration.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Represents a listener that can be removed by calling remove. */
+@protocol FIRListenerRegistration <NSObject>
+
+/**
+ * Removes the listener being tracked by this FIRListenerRegistration. After the initial call,
+ * subsequent calls have no effect.
+ */
+- (void)remove;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRQuery.h b/Firestore/Source/Public/FIRQuery.h
new file mode 100644
index 0000000..5c5546d
--- /dev/null
+++ b/Firestore/Source/Public/FIRQuery.h
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FIRFirestoreSwiftNameSupport.h"
+#import "FIRListenerRegistration.h"
+
+@class FIRFieldPath;
+@class FIRFirestore;
+@class FIRQuerySnapshot;
+@class FIRDocumentSnapshot;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Options for use with `[FIRQuery addSnapshotListener]` to control the behavior of the snapshot
+ * listener.
+ */
+FIR_SWIFT_NAME(QueryListenOptions)
+@interface FIRQueryListenOptions : NSObject
+
++ (instancetype)options NS_SWIFT_UNAVAILABLE("Use initializer");
+
+- (instancetype)init;
+
+@property(nonatomic, assign, readonly) BOOL includeQueryMetadataChanges;
+
+/**
+ * Sets the includeQueryMetadataChanges option which controls whether metadata-only changes on the
+ * query (i.e. only `FIRQuerySnapshot.metadata` changed) should trigger snapshot events. Default is
+ * NO.
+ *
+ * @param includeQueryMetadataChanges Whether to raise events for metadata-only changes on the
+ * query.
+ * @return The receiver is returned for optional method chaining.
+ */
+- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges
+ FIR_SWIFT_NAME(includeQueryMetadataChanges(_:));
+
+@property(nonatomic, assign, readonly) BOOL includeDocumentMetadataChanges;
+
+/**
+ * Sets the includeDocumentMetadataChanges option which controls whether document metadata-only
+ * changes (i.e. only `FIRDocumentSnapshot.metadata` on a document contained in the query
+ * changed) should trigger snapshot events. Default is NO.
+ *
+ * @param includeDocumentMetadataChanges Whether to raise events for document metadata-only changes.
+ * @return The receiver is returned for optional method chaining.
+ */
+- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges
+ FIR_SWIFT_NAME(includeDocumentMetadataChanges(_:));
+
+@end
+
+typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot,
+ NSError *_Nullable error);
+
+/**
+ * A `FIRQuery` refers to a Query which you can read or listen to. You can also construct
+ * refined `FIRQuery` objects by adding filters and ordering.
+ */
+FIR_SWIFT_NAME(Query)
+@interface FIRQuery : NSObject
+/** */
+- (id)init __attribute__((unavailable("FIRQuery cannot be created directly.")));
+
+/** The `FIRFirestore` for the Firestore database (useful for performing transactions, etc.). */
+@property(nonatomic, strong, readonly) FIRFirestore *firestore;
+
+#pragma mark - Retrieving Data
+/**
+ * Reads the documents matching this query.
+ *
+ * @param completion a block to execute once the documents have been successfully read.
+ * documentSet will be `nil` only if error is `non-nil`.
+ */
+- (void)getDocumentsWithCompletion:(FIRQuerySnapshotBlock)completion
+ FIR_SWIFT_NAME(getDocuments(completion:));
+
+/**
+ * Attaches a listener for QuerySnapshot events.
+ *
+ * @param listener The listener to attach.
+ *
+ * @return A FIRListenerRegistration that can be used to remove this listener.
+ */
+- (id<FIRListenerRegistration>)addSnapshotListener:(FIRQuerySnapshotBlock)listener
+ FIR_SWIFT_NAME(addSnapshotListener(_:));
+
+/**
+ * Attaches a listener for QuerySnapshot events.
+ *
+ * @param options Options controlling the listener behavior.
+ * @param listener The listener to attach.
+ *
+ * @return A FIRListenerRegistration that can be used to remove this listener.
+ */
+// clang-format off
+- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions:
+ (nullable FIRQueryListenOptions *)options
+ listener:(FIRQuerySnapshotBlock)listener
+ FIR_SWIFT_NAME(addSnapshotListener(options:listener:));
+// clang-format on
+
+#pragma mark - Filtering Data
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must be equal to the specified value.
+ *
+ * @param field The name of the field to compare.
+ * @param value The value the field must be equal to.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereField:(NSString *)field
+ isEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isEqualTo:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must be equal to the specified value.
+ *
+ * @param path The path of the field to compare.
+ * @param value The value the field must be equal to.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
+ isEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isEqualTo:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must be less than the specified value.
+ *
+ * @param field The name of the field to compare.
+ * @param value The value the field must be less than.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereField:(NSString *)field
+ isLessThan:(id)value FIR_SWIFT_NAME(whereField(_:isLessThan:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must be less than the specified value.
+ *
+ * @param path The path of the field to compare.
+ * @param value The value the field must be less than.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
+ isLessThan:(id)value FIR_SWIFT_NAME(whereField(_:isLessThan:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must be less than or equal to the specified value.
+ *
+ * @param field The name of the field to compare
+ * @param value The value the field must be less than or equal to.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereField:(NSString *)field
+ isLessThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must be less than or equal to the specified value.
+ *
+ * @param path The path of the field to compare
+ * @param value The value the field must be less than or equal to.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
+ isLessThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must greater than the specified value.
+ *
+ * @param field The name of the field to compare
+ * @param value The value the field must be greater than.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereField:(NSString *)field
+ isGreaterThan:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThan:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must greater than the specified value.
+ *
+ * @param path The path of the field to compare
+ * @param value The value the field must be greater than.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
+ isGreaterThan:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThan:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must be greater than or equal to the specified value.
+ *
+ * @param field The name of the field to compare
+ * @param value The value the field must be greater than.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereField:(NSString *)field
+ isGreaterThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * contain the specified field and the value must be greater than or equal to the specified value.
+ *
+ * @param path The path of the field to compare
+ * @param value The value the field must be greater than.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
+ isGreaterThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:));
+// clang-format on
+
+#pragma mark - Sorting Data
+/**
+ * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field.
+ *
+ * @param field The field to sort by.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryOrderedByField:(NSString *)field FIR_SWIFT_NAME(order(by:));
+
+/**
+ * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field.
+ *
+ * @param path The field to sort by.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)path FIR_SWIFT_NAME(order(by:));
+
+/**
+ * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field,
+ * optionally in descending order instead of ascending.
+ *
+ * @param field The field to sort by.
+ * @param descending Whether to sort descending.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryOrderedByField:(NSString *)field
+ descending:(BOOL)descending FIR_SWIFT_NAME(order(by:descending:));
+// clang-format on
+
+/**
+ * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field,
+ * optionally in descending order instead of ascending.
+ *
+ * @param path The field to sort by.
+ * @param descending Whether to sort descending.
+ *
+ * @return The created `FIRQuery`.
+ */
+// clang-format off
+- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)path
+ descending:(BOOL)descending FIR_SWIFT_NAME(order(by:descending:));
+// clang-format on
+
+#pragma mark - Limiting Data
+/**
+ * Creates and returns a new `FIRQuery` that's additionally limited to only return up to
+ * the specified number of documents.
+ *
+ * @param limit The maximum number of items to return.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryLimitedTo:(NSInteger)limit FIR_SWIFT_NAME(limit(to:));
+
+#pragma mark - Choosing Endpoints
+/**
+ * Creates and returns a new `FIRQuery` that starts at the provided document (inclusive). The
+ * starting position is relative to the order of the query. The document must contain all of the
+ * fields provided in the orderBy of this query.
+ *
+ * @param document The snapshot of the document to start at.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)document
+ FIR_SWIFT_NAME(start(atDocument:));
+
+/**
+ * Creates and returns a new `FIRQuery` that starts at the provided fields relative to the order of
+ * the query. The order of the field values must match the order of the order by clauses of the
+ * query.
+ *
+ * @param fieldValues The field values to start this query at, in order of the query's order by.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues FIR_SWIFT_NAME(start(at:));
+
+/**
+ * Creates and returns a new `FIRQuery` that starts after the provided document (exclusive). The
+ * starting position is relative to the order of the query. The document must contain all of the
+ * fields provided in the orderBy of this query.
+ *
+ * @param document The snapshot of the document to start after.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)document
+ FIR_SWIFT_NAME(start(afterDocument:));
+
+/**
+ * Creates and returns a new `FIRQuery` that starts after the provided fields relative to the order
+ * of the query. The order of the field values must match the order of the order by clauses of the
+ * query.
+ *
+ * @param fieldValues The field values to start this query after, in order of the query's order
+ * by.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues FIR_SWIFT_NAME(start(after:));
+
+/**
+ * Creates and returns a new `FIRQuery` that ends before the provided document (exclusive). The end
+ * position is relative to the order of the query. The document must contain all of the fields
+ * provided in the orderBy of this query.
+ *
+ * @param document The snapshot of the document to end before.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)document
+ FIR_SWIFT_NAME(end(beforeDocument:));
+
+/**
+ * Creates and returns a new `FIRQuery` that ends before the provided fields relative to the order
+ * of the query. The order of the field values must match the order of the order by clauses of the
+ * query.
+ *
+ * @param fieldValues The field values to end this query before, in order of the query's order by.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues FIR_SWIFT_NAME(end(before:));
+
+/**
+ * Creates and returns a new `FIRQuery` that ends at the provided document (exclusive). The end
+ * position is relative to the order of the query. The document must contain all of the fields
+ * provided in the orderBy of this query.
+ *
+ * @param document The snapshot of the document to end at.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)document
+ FIR_SWIFT_NAME(end(atDocument:));
+
+/**
+ * Creates and returns a new `FIRQuery` that ends at the provided fields relative to the order of
+ * the query. The order of the field values must match the order of the order by clauses of the
+ * query.
+ *
+ * @param fieldValues The field values to end this query at, in order of the query's order by.
+ *
+ * @return The created `FIRQuery`.
+ */
+- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues FIR_SWIFT_NAME(end(at:));
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRQuerySnapshot.h b/Firestore/Source/Public/FIRQuerySnapshot.h
new file mode 100644
index 0000000..800368d
--- /dev/null
+++ b/Firestore/Source/Public/FIRQuerySnapshot.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDocumentChange;
+@class FIRDocumentSnapshot;
+@class FIRQuery;
+@class FIRSnapshotMetadata;
+
+/**
+ * A `FIRQuerySnapshot` contains zero or more `FIRDocumentSnapshot` objects. It can be enumerated
+ * using "for ... in documentSet.documents" and its size can be inspected with `isEmpty` and
+ * `count`.
+ */
+FIR_SWIFT_NAME(QuerySnapshot)
+@interface FIRQuerySnapshot : NSObject
+
+/** */
+- (id)init __attribute__((unavailable("FIRQuerySnapshot cannot be created directly.")));
+
+/**
+ * The query on which you called `getDocuments` or listened to in order to get this
+ * `FIRQuerySnapshot`.
+ */
+@property(nonatomic, strong, readonly) FIRQuery *query;
+
+/** Metadata about this snapshot, concerning its source and if it has local modifications. */
+@property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata;
+
+/** Indicates whether this `FIRQuerySnapshot` is empty (contains no documents). */
+@property(nonatomic, readonly, getter=isEmpty) BOOL empty;
+
+/** The count of documents in this `FIRQuerySnapshot`. */
+@property(nonatomic, readonly) NSInteger count;
+
+/** An Array of the `FIRDocumentSnapshots` that make up this document set. */
+@property(nonatomic, strong, readonly) NSArray<FIRDocumentSnapshot *> *documents;
+
+/**
+ * An array of the documents that changed since the last snapshot. If this is the first snapshot,
+ * all documents will be in the list as Added changes.
+ */
+@property(nonatomic, strong, readonly) NSArray<FIRDocumentChange *> *documentChanges;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRSetOptions.h b/Firestore/Source/Public/FIRSetOptions.h
new file mode 100644
index 0000000..c865e06
--- /dev/null
+++ b/Firestore/Source/Public/FIRSetOptions.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An options object that configures the behavior of setData() calls. By providing the
+ * `FIRSetOptions` objects returned by `merge:`, the setData() methods in `FIRDocumentReference`,
+ * `FIRWriteBatch` and `FIRTransaction` can be configured to perform granular merges instead
+ * of overwriting the target documents in their entirety.
+ */
+NS_SWIFT_NAME(SetOptions)
+@interface FIRSetOptions : NSObject
+
+/** */
+- (id)init NS_UNAVAILABLE;
+/**
+ * Changes the behavior of setData() calls to only replace the values specified in its data
+ * argument. Fields with no corresponding values in the data passed to setData() will remain
+ * untouched.
+ *
+ * @return The created `FIRSetOptions` object
+ */
++ (instancetype)merge;
+
+/** Whether setData() should merge existing data instead of performing an overwrite. */
+@property(nonatomic, readonly, getter=isMerge) BOOL merge;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRSnapshotMetadata.h b/Firestore/Source/Public/FIRSnapshotMetadata.h
new file mode 100644
index 0000000..7fdd49c
--- /dev/null
+++ b/Firestore/Source/Public/FIRSnapshotMetadata.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Metadata about a snapshot, describing the state of the snapshot. */
+@interface FIRSnapshotMetadata : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Returns YES if the snapshot contains the result of local writes (e.g. set() or update() calls)
+ * that have not yet been committed to the backend. If your listener has opted into metadata updates
+ * (via `FIRDocumentListenOptions` or `FIRQueryListenOptions`) you will receive another snapshot
+ * with `hasPendingWrites` equal to NO once the writes have been committed to the backend.
+ */
+@property(nonatomic, assign, readonly, getter=hasPendingWrites) BOOL pendingWrites;
+
+/**
+ * Returns YES if the snapshot was created from cached data rather than guaranteed up-to-date server
+ * data. If your listener has opted into metadata updates (via `FIRDocumentListenOptions` or
+ * `FIRQueryListenOptions`) you will receive another snapshot with `isFromCache` equal to NO once
+ * the client has received up-to-date data from the backend.
+ */
+@property(nonatomic, assign, readonly, getter=isFromCache) BOOL fromCache;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRTransaction.h b/Firestore/Source/Public/FIRTransaction.h
new file mode 100644
index 0000000..68e4600
--- /dev/null
+++ b/Firestore/Source/Public/FIRTransaction.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDocumentReference;
+@class FIRDocumentSnapshot;
+@class FIRSetOptions;
+
+/**
+ * `FIRTransaction` provides methods to read and write data within a transaction.
+ *
+ * @see FIRFirestore#transaction:completion:
+ */
+FIR_SWIFT_NAME(Transaction)
+@interface FIRTransaction : NSObject
+
+/** */
+- (id)init __attribute__((unavailable("FIRTransaction cannot be created directly.")));
+
+/**
+ * Writes to the document referred to by `document`. If the document doesn't yet exist,
+ * this method creates it and then sets the data. If the document exists, this method overwrites
+ * the document data with the new values.
+ *
+ * @param data An `NSDictionary` that contains the fields and data to write to the document.
+ * @param document A reference to the document whose data should be overwritten.
+ * @return This `FIRTransaction` instance. Used for chaining method calls.
+ */
+// clang-format off
+- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document
+ FIR_SWIFT_NAME(setData(_:forDocument:));
+// clang-format on
+
+/**
+ * Writes to the document referred to by `document`. If the document doesn't yet exist,
+ * this method creates it and then sets the data. If you pass `FIRSetOptions`, the provided data
+ * will be merged into an existing document.
+ *
+ * @param data An `NSDictionary` that contains the fields and data to write to the document.
+ * @param document A reference to the document whose data should be overwritten.
+ * @param options A `FIRSetOptions` used to configure the set behavior.
+ * @return This `FIRTransaction` instance. Used for chaining method calls.
+ */
+// clang-format off
+- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document
+ options:(FIRSetOptions *)options
+ FIR_SWIFT_NAME(setData(_:forDocument:options:));
+// clang-format on
+
+/**
+ * Updates fields in the document referred to by `document`.
+ * If the document does not exist, the transaction will fail.
+ *
+ * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or
+ * `FIRFieldPath`) and values with which to update the document.
+ * @param document A reference to the document whose data should be updated.
+ * @return This `FIRTransaction` instance. Used for chaining method calls.
+ */
+// clang-format off
+- (FIRTransaction *)updateData:(NSDictionary<id, id> *)fields
+ forDocument:(FIRDocumentReference *)document
+ FIR_SWIFT_NAME(updateData(_:forDocument:));
+// clang-format on
+
+/**
+ * Deletes the document referred to by `document`.
+ *
+ * @param document A reference to the document that should be deleted.
+ * @return This `FIRTransaction` instance. Used for chaining method calls.
+ */
+- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document
+ FIR_SWIFT_NAME(deleteDocument(_:));
+
+/**
+ * Reads the document referenced by `document`.
+ *
+ * @param document A reference to the document to be read.
+ * @param error An out parameter to capture an error, if one occurred.
+ */
+- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document
+ error:(NSError *__autoreleasing *)error
+ FIR_SWIFT_NAME(getDocument(_:));
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Public/FIRWriteBatch.h b/Firestore/Source/Public/FIRWriteBatch.h
new file mode 100644
index 0000000..b88e6cc
--- /dev/null
+++ b/Firestore/Source/Public/FIRWriteBatch.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRDocumentReference;
+@class FIRSetOptions;
+
+/**
+ * A write batch is used to perform multiple writes as a single atomic unit.
+ *
+ * A WriteBatch object can be acquired by calling [FIRFirestore batch]. It provides methods for
+ * adding writes to the write batch. None of the writes will be committed (or visible locally)
+ * until [FIRWriteBatch commit] is called.
+ *
+ * Unlike transactions, write batches are persisted offline and therefore are preferable when you
+ * don't need to condition your writes on read data.
+ */
+FIR_SWIFT_NAME(WriteBatch)
+@interface FIRWriteBatch : NSObject
+
+/** :nodoc: */
+- (id)init __attribute__((unavailable("FIRWriteBatch cannot be created directly.")));
+
+/**
+ * Writes to the document referred to by `document`. If the document doesn't yet exist,
+ * this method creates it and then sets the data. If the document exists, this method overwrites
+ * the document data with the new values.
+ *
+ * @param data An `NSDictionary` that contains the fields and data to write to the document.
+ * @param document A reference to the document whose data should be overwritten.
+ * @return This `FIRWriteBatch` instance. Used for chaining method calls.
+ */
+// clang-format off
+- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document FIR_SWIFT_NAME(setData(_:forDocument:));
+// clang-format on
+
+/**
+ * Writes to the document referred to by `document`. If the document doesn't yet exist,
+ * this method creates it and then sets the data. If you pass `FIRSetOptions`, the provided data
+ * will be merged into an existing document.
+ *
+ * @param data An `NSDictionary` that contains the fields and data to write to the document.
+ * @param document A reference to the document whose data should be overwritten.
+ * @param options A `FIRSetOptions` used to configure the set behavior.
+ * @return This `FIRWriteBatch` instance. Used for chaining method calls.
+ */
+// clang-format off
+- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document
+ options:(FIRSetOptions *)options
+ FIR_SWIFT_NAME(setData(_:forDocument:options:));
+// clang-format on
+
+/**
+ * Updates fields in the document referred to by `document`.
+ * If document does not exist, the write batch will fail.
+ *
+ * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or
+ * `FIRFieldPath`) and values with which to update the document.
+ * @param document A reference to the document whose data should be updated.
+ * @return This `FIRWriteBatch` instance. Used for chaining method calls.
+ */
+// clang-format off
+- (FIRWriteBatch *)updateData:(NSDictionary<id, id> *)fields
+ forDocument:(FIRDocumentReference *)document
+ FIR_SWIFT_NAME(updateData(_:forDocument:));
+// clang-format on
+
+/**
+ * Deletes the document referred to by `document`.
+ *
+ * @param document A reference to the document that should be deleted.
+ * @return This `FIRWriteBatch` instance. Used for chaining method calls.
+ */
+- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document
+ FIR_SWIFT_NAME(deleteDocument(_:));
+
+/**
+ * Commits all of the writes in this write batch as a single atomic unit.
+ *
+ * @param completion A block to be called once all of the writes in the batch have been
+ * successfully written to the backend as an atomic unit.
+ */
+- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTBufferedWriter.h b/Firestore/Source/Remote/FSTBufferedWriter.h
new file mode 100644
index 0000000..83fada6
--- /dev/null
+++ b/Firestore/Source/Remote/FSTBufferedWriter.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>
+#import <RxLibrary/GRXWriteable.h>
+#import <RxLibrary/GRXWriter.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A buffered GRXWriter.
+ *
+ * GRPC only allows a single message to be written to a channel at a time. While the channel is
+ * sending, GRPC sets the state of the GRXWriter representing the request stream to
+ * GRXWriterStatePaused. Once the channel is ready to accept more messages GRPC sets the state of
+ * the writer to GRXWriterStateStarted.
+ *
+ * This class is NOT thread safe, even though it is accessed from multiple threads. To conform with
+ * the contract GRPC uses, all method calls on the FSTBufferedWriter must be @synchronized on the
+ * receiver.
+ */
+@interface FSTBufferedWriter : GRXWriter <GRXWriteable>
+
+/**
+ * Writes a message into the buffer. Must be called inside an @synchronized block on the receiver.
+ */
+- (void)writeValue:(id)value;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTBufferedWriter.m b/Firestore/Source/Remote/FSTBufferedWriter.m
new file mode 100644
index 0000000..d86e03a
--- /dev/null
+++ b/Firestore/Source/Remote/FSTBufferedWriter.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 <Protobuf/GPBProtocolBuffers.h>
+
+#import "FSTBufferedWriter.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTBufferedWriter {
+ GRXWriterState _state;
+ NSMutableArray<NSData *> *_queue;
+
+ id<GRXWriteable> _writeable;
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _state = GRXWriterStateNotStarted;
+ _queue = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+#pragma mark - GRXWriteable implementation
+
+/** Push the next value of the sequence to the receiving object. */
+- (void)writeValue:(id)value {
+ if (_state == GRXWriterStateStarted && _queue.count == 0) {
+ // Skip the queue.
+ [_writeable writeValue:value];
+ } else {
+ // Buffer the new value. Note that the value is assumed to be transient and doesn't need to
+ // be copied.
+ [_queue addObject:value];
+ }
+}
+
+/**
+ * Signal that the sequence is completed, or that an error ocurred. After this message is sent to
+ * the receiver, neither it nor writeValue: may be called again.
+ */
+- (void)writesFinishedWithError:(nullable NSError *)error {
+ // Unimplemented. If we ever wanted to implement sender-side initiated half close we could do so
+ // by buffering (or sending) and error.
+ [self doesNotRecognizeSelector:_cmd];
+}
+
+#pragma mark GRXWriter implementation
+// The GRXWriter implementation defines the send side of the RPC stream. Once the RPC is ready it
+// will call startWithWriteable passing a GRXWriteable into which requests can be written but only
+// when the GRXWriter is in the started state.
+
+/**
+ * Called by GRPCCall when it is ready to accept for the first request. Requests should be written
+ * to the passed writeable.
+ *
+ * GRPCCall will synchronize on the receiver around this call.
+ */
+- (void)startWithWriteable:(id<GRXWriteable>)writeable {
+ _state = GRXWriterStateStarted;
+ _writeable = writeable;
+}
+
+/**
+ * Called by GRPCCall to implement flow control on the sending side of the stream. After each
+ * writeValue: on the requestsWriteable, GRPCCall will call setState:GRXWriterStatePaused to apply
+ * backpressure. Once the stream is ready to accept another message, GRPCCall will call
+ * setState:GRXWriterStateStarted.
+ *
+ * GRPCCall will synchronize on the receiver around this call.
+ */
+- (void)setState:(GRXWriterState)newState {
+ // Manual transitions are only allowed from the started or paused states.
+ if (_state == GRXWriterStateNotStarted || _state == GRXWriterStateFinished) {
+ return;
+ }
+
+ switch (newState) {
+ case GRXWriterStateFinished:
+ _state = newState;
+ // Per GRXWriter's contract, setting the state to Finished manually means one doesn't wish the
+ // writeable to be messaged anymore.
+ _queue = nil;
+ _writeable = nil;
+ return;
+ case GRXWriterStatePaused:
+ _state = newState;
+ return;
+ case GRXWriterStateStarted:
+ if (_state == GRXWriterStatePaused) {
+ _state = newState;
+ [self writeBufferedMessages];
+ }
+ return;
+ case GRXWriterStateNotStarted:
+ return;
+ }
+}
+
+- (void)finishWithError:(nullable NSError *)error {
+ [_writeable writesFinishedWithError:error];
+ self.state = GRXWriterStateFinished;
+}
+
+- (void)writeBufferedMessages {
+ while (_state == GRXWriterStateStarted && _queue.count > 0) {
+ id value = _queue[0];
+ [_queue removeObjectAtIndex:0];
+
+ // In addition to writing the value here GRPC will apply backpressure by pausing the GRXWriter
+ // wrapping this buffer. That writer must call -pauseMessages which will cause this loop to
+ // exit. Synchronization is not required since the callback happens within the body of the
+ // writeValue implementation.
+ [_writeable writeValue:value];
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTDatastore.h b/Firestore/Source/Remote/FSTDatastore.h
new file mode 100644
index 0000000..840d2fe
--- /dev/null
+++ b/Firestore/Source/Remote/FSTDatastore.h
@@ -0,0 +1,365 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTTypes.h"
+
+@class FSTDatabaseInfo;
+@class FSTDocumentKey;
+@class FSTDispatchQueue;
+@class FSTMutation;
+@class FSTMutationResult;
+@class FSTQueryData;
+@class FSTSnapshotVersion;
+@class FSTWatchChange;
+@class FSTWatchStream;
+@class FSTWriteStream;
+@class GRPCCall;
+@class GRXWriter;
+
+@protocol FSTCredentialsProvider;
+@protocol FSTWatchStreamDelegate;
+@protocol FSTWriteStreamDelegate;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTDatastore represents a proxy for the remote server, hiding details of the RPC layer. It:
+ *
+ * - Manages connections to the server
+ * - Authenticates to the server
+ * - Manages threading and keeps higher-level code running on the worker queue
+ * - Serializes internal model objects to and from protocol buffers
+ *
+ * The FSTDatastore is generally not responsible for understanding the higher-level protocol
+ * involved in actually making changes or reading data, and aside from the connections it manages
+ * is otherwise stateless.
+ */
+@interface FSTDatastore : NSObject
+
+/** Creates a new Datastore instance with the given database info. */
++ (instancetype)datastoreWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials;
+
+- (instancetype)init __attribute__((unavailable("Use a static constructor method.")));
+
+- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ NS_DESIGNATED_INITIALIZER;
+
+/** Converts the error to a FIRFirestoreErrorDomain error. */
++ (NSError *)firestoreErrorForError:(NSError *)error;
+
+/** Returns YES if the given error indicates the RPC associated with it may not be retried. */
++ (BOOL)isPermanentWriteError:(NSError *)error;
+
+/** Returns YES if the given error is a GRPC ABORTED error. **/
++ (BOOL)isAbortedError:(NSError *)error;
+
+/** Looks up a list of documents in datastore. */
+- (void)lookupDocuments:(NSArray<FSTDocumentKey *> *)keys
+ completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion;
+
+/** Commits data to datastore. */
+- (void)commitMutations:(NSArray<FSTMutation *> *)mutations
+ completion:(FSTVoidErrorBlock)completion;
+
+/** Creates a new watch stream. */
+- (FSTWatchStream *)createWatchStreamWithDelegate:(id<FSTWatchStreamDelegate>)delegate;
+
+/** Creates a new write stream. */
+- (FSTWriteStream *)createWriteStreamWithDelegate:(id<FSTWriteStreamDelegate>)delegate;
+
+/** The name of the database and the backend. */
+@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo;
+
+@end
+
+/**
+ * An FSTStream is an abstract base class that represents a restartable streaming RPC to the
+ * Firestore backend. It's built on top of GRPC's own support for streaming RPCs, and adds several
+ * critical features for our clients:
+ *
+ * - Restarting a stream is allowed (after failure)
+ * - Exponential backoff on failure (independent of the underlying channel)
+ * - Authentication via FSTCredentialsProvider
+ * - Dispatching all callbacks into the shared worker queue
+ *
+ * Subclasses of FSTStream implement serialization of models to and from bytes (via protocol
+ * buffers) for a specific streaming RPC and emit events specific to the stream.
+ *
+ * ## Starting and Stopping
+ *
+ * Streaming RPCs are stateful and need to be started before messages can be sent and received.
+ * The FSTStream will call its delegate's specific streamDidOpen method once the stream is ready
+ * to accept requests.
+ *
+ * Should a `start` fail, FSTStream will call its delegate's specific streamDidClose method with an
+ * NSError indicating what went wrong. The delegate is free to call start again.
+ *
+ * An FSTStream can also be explicitly stopped which indicates that the caller has discarded the
+ * stream and no further events should be emitted. Once explicitly stopped, a stream cannot be
+ * restarted.
+ *
+ * ## Subclassing Notes
+ *
+ * An implementation of FSTStream needs to implement the following methods:
+ * - `createRPCWithRequestsWriter`, should create the specific RPC (a GRPCCall object).
+ * - `handleStreamOpen`, should call through to the stream-specific streamDidOpen method.
+ * - `handleStreamMessage`, receives protocol buffer responses from GRPC and must deserialize and
+ * delegate to some stream specific response method.
+ * - `handleStreamClose`, calls through to the stream-specific streamDidClose method.
+ *
+ * Additionally, beyond these required methods, subclasses will want to implement methods that
+ * take request models, serialize them, and write them to using writeRequest:.
+ *
+ * ## RPC Message Type
+ *
+ * FSTStream intentionally uses the GRPCCall interface to GRPC directly, bypassing both GRPCProtoRPC
+ * and GRXBufferedPipe for sending data. This has been done to avoid race conditions that come out
+ * of a loosely specified locking contract on GRXWriter. There's essentially no way to safely use
+ * any of the wrapper objects for GRXWriter (that perform buffering or conversion to/from protos).
+ *
+ * See https://github.com/grpc/grpc/issues/10957 for the kinds of things we're trying to avoid.
+ */
+@interface FSTStream : NSObject
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * An abstract method used by `start` to create a streaming RPC specific to this type of stream.
+ * The RPC should be created such that requests are taken from `self`.
+ *
+ * Note that the returned GRPCCall must not be a GRPCProtoRPC, since the rest of the streaming
+ * mechanism assumes it is dealing in bytes-level requests and responses.
+ */
+- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter;
+
+/**
+ * Returns YES if `start` has been called and no error has occurred. YES indicates the stream is
+ * open or in the process of opening (which encompasses respecting backoff, getting auth tokens,
+ * and starting the actual RPC). Use `isOpen` to determine if the stream is open and ready for
+ * outbound requests.
+ */
+- (BOOL)isStarted;
+
+/** Returns YES if the underlying RPC is open and the stream is ready for outbound requests. */
+- (BOOL)isOpen;
+
+/**
+ * Starts the RPC. Only allowed if isStarted returns NO. The stream is not immediately ready for
+ * use: the delegate's watchStreamDidOpen method will be invoked when the RPC is ready for outbound
+ * requests, at which point `isOpen` will return YES.
+ *
+ * When start returns, -isStarted will return YES.
+ */
+- (void)start;
+
+/**
+ * Stops the RPC. This call is idempotent and allowed regardless of the current isStarted state.
+ *
+ * Unlike a transient stream close, stopping a stream is permanent. This is guaranteed NOT to emit
+ * any further events on the stream-specific delegate, including the streamDidClose method.
+ *
+ * NOTE: This no-events contract may seem counter-intuitive but allows the caller to
+ * straightforwardly sequence stream tear-down without having to worry about when the delegate's
+ * streamDidClose methods will get called. For example if the stream must be exchanged for another
+ * during a user change this allows `stop` to be called eagerly without worrying about the
+ * streamDidClose method accidentally restarting the stream before the new one is ready.
+ *
+ * When stop returns, -isStarted and -isOpen will both return NO.
+ */
+- (void)stop;
+
+/**
+ * After an error the stream will usually back off on the next attempt to start it. If the error
+ * warrants an immediate restart of the stream, the sender can use this to indicate that the
+ * receiver should not back off.
+ *
+ * Each error will call the stream-specific streamDidClose method. That method can decide to
+ * inhibit backoff if required.
+ */
+- (void)inhibitBackoff;
+
+@end
+
+#pragma mark - FSTWatchStream
+
+/** A protocol defining the events that can be emitted by the FSTWatchStream. */
+@protocol FSTWatchStreamDelegate <NSObject>
+
+/** Called by the FSTWatchStream when it is ready to accept outbound request messages. */
+- (void)watchStreamDidOpen;
+
+/**
+ * Called by the FSTWatchStream with changes and the snapshot versions included in in the
+ * WatchChange responses sent back by the server.
+ */
+- (void)watchStreamDidChange:(FSTWatchChange *)change
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion;
+
+/**
+ * Called by the FSTWatchStream when the underlying streaming RPC is closed for whatever reason,
+ * usually because of an error, but possibly due to an idle timeout. The error passed to this
+ * method may be nil, in which case the stream was closed without attributable fault.
+ *
+ * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping"
+ * on FSTStream for details.
+ */
+- (void)watchStreamDidClose:(NSError *_Nullable)error;
+
+@end
+
+/**
+ * An FSTStream that implements the StreamingWatch RPC.
+ *
+ * Once the FSTWatchStream has called the streamDidOpen method, any number of watchQuery and
+ * unwatchTargetId calls can be sent to control what changes will be sent from the server for
+ * WatchChanges.
+ */
+@interface FSTWatchStream : FSTStream
+
+/**
+ * Initializes the watch stream with its dependencies.
+ */
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWatchStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Registers interest in the results of the given query. If the query includes a resumeToken it
+ * will be included in the request. Results that affect the query will be streamed back as
+ * WatchChange messages that reference the targetID included in |query|.
+ */
+- (void)watchQuery:(FSTQueryData *)query;
+
+/** Unregisters interest in the results of the query associated with the given target ID. */
+- (void)unwatchTargetID:(FSTTargetID)targetID;
+
+@property(nonatomic, weak, readonly) id<FSTWatchStreamDelegate> delegate;
+
+@end
+
+#pragma mark - FSTWriteStream
+
+@protocol FSTWriteStreamDelegate <NSObject>
+
+/** Called by the FSTWriteStream when it is ready to accept outbound request messages. */
+- (void)writeStreamDidOpen;
+
+/**
+ * Called by the FSTWriteStream upon a successful handshake response from the server, which is the
+ * receiver's cue to send any pending writes.
+ */
+- (void)writeStreamDidCompleteHandshake;
+
+/**
+ * Called by the FSTWriteStream upon receiving a StreamingWriteResponse from the server that
+ * contains mutation results.
+ */
+- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)results;
+
+/**
+ * Called when the FSTWriteStream's underlying RPC is closed for whatever reason, usually because
+ * of an error, but possibly due to an idle timeout. The error passed to this method may be nil, in
+ * which case the stream was closed without attributable fault.
+ *
+ * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping"
+ * on FSTStream for details.
+ */
+- (void)writeStreamDidClose:(NSError *_Nullable)error;
+
+@end
+
+/**
+ * An FSTStream that implements the StreamingWrite RPC.
+ *
+ * The StreamingWrite RPC requires the caller to maintain special `streamToken` state in between
+ * calls, to help the server understand which responses the client has processed by the time the
+ * next request is made. Every response may contain a `streamToken`; this value must be passed to
+ * the next request.
+ *
+ * After calling `start` on this stream, the next request must be a handshake, containing whatever
+ * streamToken is on hand. Once a response to this request is received, all pending mutations may
+ * be submitted. When submitting multiple batches of mutations at the same time, it's okay to use
+ * the same streamToken for the calls to `writeMutations:`.
+ */
+@interface FSTWriteStream : FSTStream
+
+/**
+ * Initializes the write stream with its dependencies.
+ */
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWriteStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Sends an initial streamToken to the server, performing the handshake required to make the
+ * StreamingWrite RPC work. Subsequent `writeMutations:` calls should wait until a response has
+ * been delivered to the delegate's writeStreamDidCompleteHandshake method.
+ */
+- (void)writeHandshake;
+
+/** Sends a group of mutations to the Firestore backend to apply. */
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations;
+
+@property(nonatomic, weak, readonly) id<FSTWriteStreamDelegate> delegate;
+
+/**
+ * Tracks whether or not a handshake has been successfully exchanged and the stream is ready to
+ * accept mutations.
+ */
+@property(nonatomic, assign, readwrite, getter=isHandshakeComplete) BOOL handshakeComplete;
+
+/**
+ * The last received stream token from the server, used to acknowledge which responses the client
+ * has processed. Stream tokens are opaque checkpoint markers whose only real value is their
+ * inclusion in the next request.
+ *
+ * FSTWriteStream manages propagating this value from responses to the next request.
+ */
+@property(nonatomic, strong, nullable) NSData *lastStreamToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTDatastore.m b/Firestore/Source/Remote/FSTDatastore.m
new file mode 100644
index 0000000..3ed2729
--- /dev/null
+++ b/Firestore/Source/Remote/FSTDatastore.m
@@ -0,0 +1,1027 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTDatastore.h"
+
+#import <GRPCClient/GRPCCall+OAuth2.h>
+#import <GRPCClient/GRPCCall.h>
+#import <ProtoRPC/ProtoRPC.h>
+
+#import "FIRFirestore+Internal.h"
+#import "FIRFirestoreErrors.h"
+#import "FIRFirestoreVersion.h"
+#import "FSTAssert.h"
+#import "FSTBufferedWriter.h"
+#import "FSTClasses.h"
+#import "FSTCredentialsProvider.h"
+#import "FSTDatabaseID.h"
+#import "FSTDatabaseInfo.h"
+#import "FSTDispatchQueue.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTExponentialBackoff.h"
+#import "FSTLocalStore.h"
+#import "FSTLogger.h"
+#import "FSTMutation.h"
+#import "FSTQueryData.h"
+#import "FSTSerializerBeta.h"
+
+#import "Firestore.pbrpc.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// GRPC does not publicly declare a means of disabling SSL, which we need for testing. Firestore
+// directly exposes an sslEnabled setting so this is required to plumb that through. Note that our
+// own tests depend on this working so we'll know if this changes upstream.
+@interface GRPCHost
++ (nullable instancetype)hostWithAddress:(NSString *)address;
+@property(nonatomic, getter=isSecure) BOOL secure;
+@end
+
+/**
+ * Initial backoff time in seconds after an error.
+ * Set to 1s according to https://cloud.google.com/apis/design/errors.
+ */
+static const NSTimeInterval kBackoffInitialDelay = 1;
+static const NSTimeInterval kBackoffMaxDelay = 60.0;
+static const double kBackoffFactor = 1.5;
+static NSString *const kXGoogAPIClientHeader = @"x-goog-api-client";
+static NSString *const kGoogleCloudResourcePrefix = @"google-cloud-resource-prefix";
+
+/** Function typedef used to create RPCs. */
+typedef GRPCProtoCall * (^RPCFactory)();
+
+#pragma mark - FSTStream
+
+/** The state of a stream. */
+typedef NS_ENUM(NSInteger, FSTStreamState) {
+ /**
+ * The streaming RPC is not running and there's no error condition. Calling `start` will
+ * start the stream immediately without backoff. While in this state -isStarted will return NO.
+ */
+ FSTStreamStateInitial = 0,
+
+ /**
+ * The stream is starting, and is waiting for an auth token to attach to the initial request.
+ * While in this state, isStarted will return YES but isOpen will return NO.
+ */
+ FSTStreamStateAuth,
+
+ /**
+ * The streaming RPC is up and running. Requests and responses can flow freely. Both
+ * isStarted and isOpen will return YES.
+ */
+ FSTStreamStateOpen,
+
+ /**
+ * The stream encountered an error. The next start attempt will back off. While in this state
+ * -isStarted will return NO.
+ */
+ FSTStreamStateError,
+
+ /**
+ * An in-between state after an error where the stream is waiting before re-starting. After
+ * waiting is complete, the stream will try to open. While in this state -isStarted will
+ * return YES but isOpen will return NO.
+ */
+ FSTStreamStateBackoff,
+
+ /**
+ * The stream has been explicitly stopped; no further events will be emitted.
+ */
+ FSTStreamStateStopped,
+};
+
+// We need to declare these classes first so that Datastore can alloc them.
+
+@interface FSTWatchStream ()
+
+/** The delegate that will receive events generated by the watch stream. */
+@property(nonatomic, weak, nullable) id<FSTWatchStreamDelegate> delegate;
+
+@end
+
+@interface FSTBetaWatchStream : FSTWatchStream
+
+/**
+ * Initializes the watch stream with its dependencies.
+ */
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ serializer:(FSTSerializerBeta *)serializer
+ delegate:(id<FSTWatchStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWatchStreamDelegate>)delegate NS_UNAVAILABLE;
+
+@end
+
+@interface FSTWriteStream ()
+
+@property(nonatomic, weak, nullable) id<FSTWriteStreamDelegate> delegate;
+
+@end
+
+@interface FSTBetaWriteStream : FSTWriteStream
+
+/**
+ * Initializes the write stream with its dependencies.
+ */
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ serializer:(FSTSerializerBeta *)serializer
+ delegate:(id<FSTWriteStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWriteStreamDelegate>)delegate NS_UNAVAILABLE;
+
+@end
+
+@interface FSTStream () <GRXWriteable>
+
+@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo;
+@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue;
+@property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentials;
+@property(nonatomic, unsafe_unretained, readonly) Class responseMessageClass;
+@property(nonatomic, strong, readonly) FSTExponentialBackoff *backoff;
+
+/** A flag tracking whether the stream received a message from the backend. */
+@property(nonatomic, assign) BOOL messageReceived;
+
+/**
+ * Stream state as exposed to consumers of FSTStream. This differs from GRXWriter's notion of the
+ * state of the stream.
+ */
+@property(nonatomic, assign) FSTStreamState state;
+
+/** The RPC handle. Used for cancellation. */
+@property(nonatomic, strong, nullable) GRPCCall *rpc;
+
+/**
+ * The send-side of the RPC stream in which to submit requests, but only once the underlying RPC has
+ * started.
+ */
+@property(nonatomic, strong, nullable) FSTBufferedWriter *requestsWriter;
+
+@end
+
+#pragma mark - FSTDatastore
+
+@interface FSTDatastore ()
+
+/** The GRPC service for Firestore. */
+@property(nonatomic, strong, readonly) GCFSFirestore *service;
+
+@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue;
+
+/** An object for getting an auth token before each request. */
+@property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentials;
+
+@property(nonatomic, strong, readonly) FSTSerializerBeta *serializer;
+
+@end
+
+@implementation FSTDatastore
+
++ (instancetype)datastoreWithDatabase:(FSTDatabaseInfo *)databaseInfo
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials {
+ return [[FSTDatastore alloc] initWithDatabaseInfo:databaseInfo
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials];
+}
+
+- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials {
+ if (self = [super init]) {
+ _databaseInfo = databaseInfo;
+ if (!databaseInfo.isSSLEnabled) {
+ GRPCHost *hostConfig = [GRPCHost hostWithAddress:databaseInfo.host];
+ hostConfig.secure = NO;
+ }
+ _service = [GCFSFirestore serviceWithHost:databaseInfo.host];
+ _workerDispatchQueue = workerDispatchQueue;
+ _credentials = credentials;
+ _serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseInfo.databaseID];
+ }
+ return self;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FSTDatastore: %@>", self.databaseInfo];
+}
+
+/**
+ * Converts the error to an error within the domain FIRFirestoreErrorDomain.
+ */
++ (NSError *)firestoreErrorForError:(NSError *)error {
+ if (!error) {
+ return error;
+ } else if ([error.domain isEqualToString:FIRFirestoreErrorDomain]) {
+ return error;
+ } else if ([error.domain isEqualToString:kGRPCErrorDomain]) {
+ FSTAssert(error.code >= GRPCErrorCodeCancelled && error.code <= GRPCErrorCodeUnauthenticated,
+ @"Unknown GRPC error code: %ld", (long)error.code);
+ return
+ [NSError errorWithDomain:FIRFirestoreErrorDomain code:error.code userInfo:error.userInfo];
+ } else {
+ return [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeUnknown
+ userInfo:@{NSUnderlyingErrorKey : error}];
+ }
+}
+
++ (BOOL)isAbortedError:(NSError *)error {
+ FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain],
+ @"isAbortedError: only works with errors emitted by FSTDatastore.");
+ return error.code == FIRFirestoreErrorCodeAborted;
+}
+
++ (BOOL)isPermanentWriteError:(NSError *)error {
+ FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain],
+ @"isPerminanteWriteError: only works with errors emitted by FSTDatastore.");
+ switch (error.code) {
+ case FIRFirestoreErrorCodeCancelled:
+ case FIRFirestoreErrorCodeUnknown:
+ case FIRFirestoreErrorCodeDeadlineExceeded:
+ case FIRFirestoreErrorCodeResourceExhausted:
+ case FIRFirestoreErrorCodeInternal:
+ case FIRFirestoreErrorCodeUnavailable:
+ case FIRFirestoreErrorCodeUnauthenticated:
+ // Unauthenticated means something went wrong with our token and we need
+ // to retry with new credentials which will happen automatically.
+ // TODO(b/37325376): Give up after second unauthenticated error.
+ return NO;
+ case FIRFirestoreErrorCodeInvalidArgument:
+ case FIRFirestoreErrorCodeNotFound:
+ case FIRFirestoreErrorCodeAlreadyExists:
+ case FIRFirestoreErrorCodePermissionDenied:
+ case FIRFirestoreErrorCodeFailedPrecondition:
+ case FIRFirestoreErrorCodeAborted:
+ // Aborted might be retried in some scenarios, but that is dependant on
+ // the context and should handled individually by the calling code.
+ // See https://cloud.google.com/apis/design/errors
+ case FIRFirestoreErrorCodeOutOfRange:
+ case FIRFirestoreErrorCodeUnimplemented:
+ case FIRFirestoreErrorCodeDataLoss:
+ default:
+ return YES;
+ }
+}
+
+/** Returns the string to be used as x-goog-api-client header value. */
++ (NSString *)googAPIClientHeaderValue {
+ // TODO(dimond): This should ideally also include the grpc version, however, gRPC defines the
+ // version as a macro, so it would be hardcoded based on version we have at compile time of
+ // the Firestore library, rather than the version available at runtime/at compile time by the
+ // user of the library.
+ return [NSString stringWithFormat:@"gl-objc/ fire/%s grpc/", FirebaseFirestoreVersionString];
+}
+
+/** Returns the string to be used as google-cloud-resource-prefix header value. */
++ (NSString *)googleCloudResourcePrefixForDatabaseID:(FSTDatabaseID *)databaseID {
+ return [NSString
+ stringWithFormat:@"projects/%@/databases/%@", databaseID.projectID, databaseID.databaseID];
+}
+/**
+ * Takes a dictionary of (HTTP) response headers and returns the set of whitelisted headers
+ * (for logging purposes).
+ */
++ (NSDictionary<NSString *, NSString *> *)extractWhiteListedHeaders:
+ (NSDictionary<NSString *, NSString *> *)headers {
+ NSMutableDictionary<NSString *, NSString *> *whiteListedHeaders =
+ [NSMutableDictionary dictionary];
+ NSArray<NSString *> *whiteList = @[
+ @"date", @"x-google-backends", @"x-google-netmon-label", @"x-google-service",
+ @"x-google-gfe-request-trace"
+ ];
+ [headers
+ enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) {
+ if ([whiteList containsObject:[headerName lowercaseString]]) {
+ whiteListedHeaders[headerName] = headerValue;
+ }
+ }];
+ return whiteListedHeaders;
+}
+
+/** Logs the (whitelisted) headers returned for an GRPCProtoCall RPC. */
++ (void)logHeadersForRPC:(GRPCProtoCall *)rpc RPCName:(NSString *)rpcName {
+ if ([FIRFirestore isLoggingEnabled]) {
+ FSTLog(@"RPC %@ returned headers (whitelisted): %@", rpcName,
+ [FSTDatastore extractWhiteListedHeaders:rpc.responseHeaders]);
+ }
+}
+
+- (void)commitMutations:(NSArray<FSTMutation *> *)mutations
+ completion:(FSTVoidErrorBlock)completion {
+ GCFSCommitRequest *request = [GCFSCommitRequest message];
+ request.database = [self.serializer encodedDatabaseID];
+
+ NSMutableArray<GCFSWrite *> *mutationProtos = [NSMutableArray array];
+ for (FSTMutation *mutation in mutations) {
+ [mutationProtos addObject:[self.serializer encodedMutation:mutation]];
+ }
+ request.writesArray = mutationProtos;
+
+ RPCFactory rpcFactory = ^GRPCProtoCall * {
+ __block GRPCProtoCall *rpc = [self.service
+ RPCToCommitWithRequest:request
+ handler:^(GCFSCommitResponse *response, NSError *_Nullable error) {
+ error = [FSTDatastore firestoreErrorForError:error];
+ [self.workerDispatchQueue dispatchAsync:^{
+ FSTLog(@"RPC CommitRequest completed. Error: %@", error);
+ [FSTDatastore logHeadersForRPC:rpc RPCName:@"CommitRequest"];
+ completion(error);
+ }];
+ }];
+ return rpc;
+ };
+
+ [self invokeRPCWithFactory:rpcFactory errorHandler:completion];
+}
+
+- (void)lookupDocuments:(NSArray<FSTDocumentKey *> *)keys
+ completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion {
+ GCFSBatchGetDocumentsRequest *request = [GCFSBatchGetDocumentsRequest message];
+ request.database = [self.serializer encodedDatabaseID];
+ for (FSTDocumentKey *key in keys) {
+ [request.documentsArray addObject:[self.serializer encodedDocumentKey:key]];
+ }
+
+ __block FSTMaybeDocumentDictionary *results =
+ [FSTMaybeDocumentDictionary maybeDocumentDictionary];
+
+ RPCFactory rpcFactory = ^GRPCProtoCall * {
+ __block GRPCProtoCall *rpc = [self.service
+ RPCToBatchGetDocumentsWithRequest:request
+ eventHandler:^(BOOL done,
+ GCFSBatchGetDocumentsResponse *_Nullable response,
+ NSError *_Nullable error) {
+ error = [FSTDatastore firestoreErrorForError:error];
+ [self.workerDispatchQueue dispatchAsync:^{
+ if (error) {
+ FSTLog(@"RPC BatchGetDocuments completed. Error: %@", error);
+ [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"];
+ completion(nil, error);
+ return;
+ }
+
+ if (!done) {
+ // Streaming response, accumulate result
+ FSTMaybeDocument *doc =
+ [self.serializer decodedMaybeDocumentFromBatch:response];
+ results = [results dictionaryBySettingObject:doc forKey:doc.key];
+ } else {
+ // Streaming response is done, call completion
+ FSTLog(@"RPC BatchGetDocuments completed successfully.");
+ [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"];
+ FSTAssert(!response, @"Got response after done.");
+ NSMutableArray<FSTMaybeDocument *> *docs =
+ [NSMutableArray arrayWithCapacity:keys.count];
+ for (FSTDocumentKey *key in keys) {
+ [docs addObject:results[key]];
+ }
+ completion(docs, nil);
+ }
+ }];
+ }];
+ return rpc;
+ };
+
+ [self invokeRPCWithFactory:rpcFactory
+ errorHandler:^(NSError *_Nonnull error) {
+ error = [FSTDatastore firestoreErrorForError:error];
+ completion(nil, error);
+ }];
+}
+
+- (void)invokeRPCWithFactory:(GRPCProtoCall * (^)())rpcFactory
+ errorHandler:(FSTVoidErrorBlock)errorHandler {
+ // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token,
+ // but I'm not sure how to detect that right now. http://b/32762461
+ [self.credentials
+ getTokenForcingRefresh:NO
+ completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) {
+ error = [FSTDatastore firestoreErrorForError:error];
+ [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{
+ if (error) {
+ errorHandler(error);
+ } else {
+ GRPCProtoCall *rpc = rpcFactory();
+ [FSTDatastore prepareHeadersForRPC:rpc
+ databaseID:self.databaseInfo.databaseID
+ token:result.token];
+ [rpc start];
+ }
+ }];
+ }];
+}
+
+- (FSTWatchStream *)createWatchStreamWithDelegate:(id<FSTWatchStreamDelegate>)delegate {
+ return [[FSTBetaWatchStream alloc] initWithDatabase:_databaseInfo
+ workerDispatchQueue:_workerDispatchQueue
+ credentials:_credentials
+ serializer:_serializer
+ delegate:delegate];
+}
+
+- (FSTWriteStream *)createWriteStreamWithDelegate:(id<FSTWriteStreamDelegate>)delegate {
+ return [[FSTBetaWriteStream alloc] initWithDatabase:_databaseInfo
+ workerDispatchQueue:_workerDispatchQueue
+ credentials:_credentials
+ serializer:_serializer
+ delegate:delegate];
+}
+
+/** Adds headers to the RPC including any OAuth access token if provided .*/
++ (void)prepareHeadersForRPC:(GRPCCall *)rpc
+ databaseID:(FSTDatabaseID *)databaseID
+ token:(nullable NSString *)token {
+ rpc.oauth2AccessToken = token;
+ rpc.requestHeaders[kXGoogAPIClientHeader] = [FSTDatastore googAPIClientHeaderValue];
+ // This header is used to improve routing and project isolation by the backend.
+ rpc.requestHeaders[kGoogleCloudResourcePrefix] =
+ [FSTDatastore googleCloudResourcePrefixForDatabaseID:databaseID];
+}
+
+@end
+
+#pragma mark - FSTStream
+
+@implementation FSTStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass {
+ if (self = [super init]) {
+ _databaseInfo = database;
+ _workerDispatchQueue = workerDispatchQueue;
+ _credentials = credentials;
+ _responseMessageClass = responseMessageClass;
+
+ _backoff = [FSTExponentialBackoff exponentialBackoffWithDispatchQueue:workerDispatchQueue
+ initialDelay:kBackoffInitialDelay
+ backoffFactor:kBackoffFactor
+ maxDelay:kBackoffMaxDelay];
+ _state = FSTStreamStateInitial;
+ }
+ return self;
+}
+
+- (BOOL)isStarted {
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+ FSTStreamState state = self.state;
+ return state == FSTStreamStateBackoff || state == FSTStreamStateAuth ||
+ state == FSTStreamStateOpen;
+}
+
+- (BOOL)isOpen {
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+ return self.state == FSTStreamStateOpen;
+}
+
+- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)start {
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ if (self.state == FSTStreamStateError) {
+ [self performBackoff];
+ return;
+ }
+
+ FSTLog(@"%@ %p start", NSStringFromClass([self class]), (__bridge void *)self);
+ FSTAssert(self.state == FSTStreamStateInitial, @"Already started");
+
+ self.state = FSTStreamStateAuth;
+
+ [self.credentials
+ getTokenForcingRefresh:NO
+ completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) {
+ error = [FSTDatastore firestoreErrorForError:error];
+ [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{
+ [self resumeStartWithToken:result error:error];
+ }];
+ }];
+}
+
+/** Add an access token to our RPC, after obtaining one from the credentials provider. */
+- (void)resumeStartWithToken:(FSTGetTokenResult *)token error:(NSError *)error {
+ if (self.state == FSTStreamStateStopped) {
+ // Streams can be stopped while waiting for authorization.
+ return;
+ }
+
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+ FSTAssert(self.state == FSTStreamStateAuth, @"State should still be auth (was %ld)",
+ (long)self.state);
+
+ // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token,
+ // but I'm not sure how to detect that right now. http://b/32762461
+ if (error) {
+ // RPC has not been started yet, so just invoke higher-level close handler.
+ [self handleStreamClose:error];
+ return;
+ }
+
+ self.requestsWriter = [[FSTBufferedWriter alloc] init];
+ _rpc = [self createRPCWithRequestsWriter:self.requestsWriter];
+ [FSTDatastore prepareHeadersForRPC:_rpc
+ databaseID:self.databaseInfo.databaseID
+ token:token.token];
+ [_rpc startWithWriteable:self];
+
+ self.state = FSTStreamStateOpen;
+ [self handleStreamOpen];
+}
+
+/** Backs off after an error. */
+- (void)performBackoff {
+ FSTLog(@"%@ %p backoff", NSStringFromClass([self class]), (__bridge void *)self);
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ FSTAssert(self.state == FSTStreamStateError, @"Should only perform backoff in an error case");
+ self.state = FSTStreamStateBackoff;
+
+ FSTWeakify(self);
+ [self.backoff backoffAndRunBlock:^{
+ FSTStrongify(self);
+ [self resumeStartFromBackoff];
+ }];
+}
+
+/** Resumes stream start after backing off. */
+- (void)resumeStartFromBackoff {
+ if (self.state == FSTStreamStateStopped) {
+ // Streams can be stopped while waiting for backoff to complete.
+ return;
+ }
+
+ // In order to have performed a backoff the stream must have been in an error state just prior
+ // to entering the backoff state. If we weren't stopped we must be in the backoff state.
+ FSTAssert(self.state == FSTStreamStateBackoff, @"State should still be backoff (was %ld)",
+ (long)self.state);
+
+ // Momentarily set state to FSTStreamStateInitial as `start` expects it.
+ self.state = FSTStreamStateInitial;
+ [self start];
+ FSTAssert([self isStarted], @"Stream should have started.");
+}
+
+- (void)stop {
+ FSTLog(@"%@ %p stop", NSStringFromClass([self class]), (__bridge void *)self);
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ // Prevent any possible future restart of this stream.
+ self.state = FSTStreamStateStopped;
+
+ // Close the stream client side.
+ FSTBufferedWriter *requestsWriter = self.requestsWriter;
+ @synchronized(requestsWriter) {
+ [requestsWriter finishWithError:nil];
+ }
+}
+
+- (void)inhibitBackoff {
+ FSTAssert(![self isStarted], @"Can only inhibit backoff after an error (was %ld)",
+ (long)self.state);
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ // Clear the error condition.
+ self.state = FSTStreamStateInitial;
+ [self.backoff reset];
+}
+
+/**
+ * Parses a protocol buffer response from the server. If the message fails to parse, generates
+ * an error and closes the stream.
+ *
+ * @param protoClass A protocol buffer message class object, that responds to parseFromData:error:.
+ * @param data The bytes in the response as returned from GRPC.
+ * @return An instance of the protocol buffer message, parsed from the data if parsing was
+ * successful, or nil otherwise.
+ */
+- (nullable id)parseProto:(Class)protoClass data:(NSData *)data error:(NSError **)error {
+ NSError *parseError;
+ id parsed = [protoClass parseFromData:data error:&parseError];
+ if (parsed) {
+ *error = nil;
+ return parsed;
+ } else {
+ NSDictionary *info = @{
+ NSLocalizedDescriptionKey : @"Unable to parse response from the server",
+ NSUnderlyingErrorKey : parseError,
+ @"Expected class" : protoClass,
+ @"Received value" : data,
+ };
+ *error = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeInternal
+ userInfo:info];
+ return nil;
+ }
+}
+
+/**
+ * Writes a request proto into the stream.
+ */
+- (void)writeRequest:(GPBMessage *)request {
+ NSData *data = [request data];
+
+ FSTBufferedWriter *requestsWriter = self.requestsWriter;
+ @synchronized(requestsWriter) {
+ [requestsWriter writeValue:data];
+ }
+}
+
+#pragma mark Template methods for subclasses
+
+/**
+ * Called by the stream after the stream has been successfully connected, authenticated, and is now
+ * ready to accept messages.
+ *
+ * Subclasses should relay to their stream-specific delegate. Calling [super handleStreamOpen] is
+ * not required.
+ */
+- (void)handleStreamOpen {
+}
+
+/**
+ * Called by the stream for each incoming protocol message coming from the server.
+ *
+ * Subclasses should implement this to deserialize the value and relay to their stream-specific
+ * delegate, if appropriate. Calling [super handleStreamMessage] is not required.
+ */
+- (void)handleStreamMessage:(id)value {
+}
+
+/**
+ * Called by the stream when the underlying RPC has been closed for whatever reason.
+ *
+ * Subclasses should first call [super handleStreamClose:] and then call to their
+ * stream-specific delegate.
+ */
+- (void)handleStreamClose:(NSError *_Nullable)error {
+ FSTLog(@"%@ %p close: %@", NSStringFromClass([self class]), (__bridge void *)self, error);
+ FSTAssert([self isStarted], @"Can't handle server close in non-started state.");
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ self.messageReceived = NO;
+ self.rpc = nil;
+ self.requestsWriter = nil;
+
+ // In theory the stream could close cleanly, however, in our current model we never expect this
+ // to happen because if we stop a stream ourselves, this callback will never be called. To
+ // prevent cases where we retry without a backoff accidentally, we set the stream to error
+ // in all cases.
+ self.state = FSTStreamStateError;
+
+ if (error.code == FIRFirestoreErrorCodeResourceExhausted) {
+ FSTLog(@"%@ %p Using maximum backoff delay to prevent overloading the backend.", [self class],
+ (__bridge void *)self);
+ [self.backoff resetToMax];
+ }
+}
+
+#pragma mark GRXWriteable implementation
+// The GRXWriteable implementation defines the receive side of the RPC stream.
+
+/**
+ * Called by GRPC when it publishes a value. It is called from GRPC's own queue so we immediately
+ * redispatch back onto our own worker queue.
+ */
+- (void)writeValue:(id)value __used {
+ // TODO(mcg): remove the double-dispatch once GRPCCall at head is released.
+ // Once released we can set the responseDispatchQueue property on the GRPCCall and then this
+ // method can call handleStreamMessage directly.
+ FSTWeakify(self);
+ [self.workerDispatchQueue dispatchAsync:^{
+ FSTStrongify(self);
+ if (!self || self.state == FSTStreamStateStopped) {
+ return;
+ }
+ if (!self.messageReceived) {
+ self.messageReceived = YES;
+ if ([FIRFirestore isLoggingEnabled]) {
+ FSTLog(@"%@ %p headers (whitelisted): %@", NSStringFromClass([self class]),
+ (__bridge void *)self,
+ [FSTDatastore extractWhiteListedHeaders:self.rpc.responseHeaders]);
+ }
+ }
+ NSError *error;
+ id proto = [self parseProto:self.responseMessageClass data:value error:&error];
+ if (proto) {
+ [self handleStreamMessage:proto];
+ } else {
+ [_rpc finishWithError:error];
+ }
+ }];
+}
+
+/**
+ * Called by GRPC when it closed the stream with an error representing the final state of the
+ * stream.
+ *
+ * Do not call directly, since it dispatches via the worker queue. Call handleStreamClose to
+ * directly inform stream-specific logic, or call stop to tear down the stream.
+ */
+- (void)writesFinishedWithError:(NSError *_Nullable)error __used {
+ error = [FSTDatastore firestoreErrorForError:error];
+ FSTWeakify(self);
+ [self.workerDispatchQueue dispatchAsync:^{
+ FSTStrongify(self);
+ if (!self || self.state == FSTStreamStateStopped) {
+ return;
+ }
+ [self handleStreamClose:error];
+ }];
+}
+
+@end
+
+#pragma mark - FSTWatchStream
+
+@implementation FSTWatchStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWatchStreamDelegate>)delegate {
+ self = [super initWithDatabase:database
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials
+ responseMessageClass:responseMessageClass];
+ if (self) {
+ _delegate = delegate;
+ }
+ return self;
+}
+
+- (void)stop {
+ // Clear the delegate to avoid any possible bleed through of events from GRPC.
+ self.delegate = nil;
+
+ [super stop];
+}
+
+- (void)watchQuery:(FSTQueryData *)query {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)unwatchTargetID:(FSTTargetID)targetID {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)handleStreamOpen {
+ [self.delegate watchStreamDidOpen];
+}
+
+- (void)handleStreamClose:(NSError *_Nullable)error {
+ [super handleStreamClose:error];
+ [self.delegate watchStreamDidClose:error];
+}
+
+@end
+
+#pragma mark - FSTBetaWatchStream
+
+@implementation FSTBetaWatchStream {
+ FSTSerializerBeta *_serializer;
+}
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ serializer:(FSTSerializerBeta *)serializer
+ delegate:(id<FSTWatchStreamDelegate>)delegate {
+ self = [super initWithDatabase:database
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials
+ responseMessageClass:[GCFSListenResponse class]
+ delegate:delegate];
+ if (self) {
+ _serializer = serializer;
+ }
+ return self;
+}
+
+- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter {
+ return [[GRPCCall alloc] initWithHost:self.databaseInfo.host
+ path:@"/google.firestore.v1beta1.Firestore/Listen"
+ requestsWriter:requestsWriter];
+}
+
+- (void)watchQuery:(FSTQueryData *)query {
+ FSTAssert([self isOpen], @"Not yet open");
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ GCFSListenRequest *request = [GCFSListenRequest message];
+ request.database = [_serializer encodedDatabaseID];
+ request.addTarget = [_serializer encodedTarget:query];
+ request.labels = [_serializer encodedListenRequestLabelsForQueryData:query];
+
+ FSTLog(@"FSTWatchStream %p watch: %@", (__bridge void *)self, request);
+ [self writeRequest:request];
+}
+
+- (void)unwatchTargetID:(FSTTargetID)targetID {
+ FSTAssert([self isOpen], @"Not yet open");
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ GCFSListenRequest *request = [GCFSListenRequest message];
+ request.database = [_serializer encodedDatabaseID];
+ request.removeTarget = targetID;
+
+ FSTLog(@"FSTWatchStream %p unwatch: %@", (__bridge void *)self, request);
+ [self writeRequest:request];
+}
+
+/**
+ * Receives an inbound message from GRPC, deserializes, and then passes that on to the delegate's
+ * watchStreamDidChange:snapshotVersion: callback.
+ */
+- (void)handleStreamMessage:(GCFSListenResponse *)proto {
+ FSTLog(@"FSTWatchStream %p response: %@", (__bridge void *)self, proto);
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ // A successful response means the stream is healthy.
+ [self.backoff reset];
+
+ FSTWatchChange *change = [_serializer decodedWatchChange:proto];
+ FSTSnapshotVersion *snap = [_serializer versionFromListenResponse:proto];
+ [self.delegate watchStreamDidChange:change snapshotVersion:snap];
+}
+
+@end
+
+#pragma mark - FSTWriteStream
+
+@implementation FSTWriteStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWriteStreamDelegate>)delegate {
+ self = [super initWithDatabase:database
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials
+ responseMessageClass:responseMessageClass];
+ if (self) {
+ _delegate = delegate;
+ }
+ return self;
+}
+
+- (void)start {
+ self.handshakeComplete = NO;
+ [super start];
+}
+
+- (void)stop {
+ // Clear the delegate to avoid any possible bleed through of events from GRPC.
+ self.delegate = nil;
+
+ [super stop];
+}
+
+- (void)writeHandshake {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)handleStreamOpen {
+ [self.delegate writeStreamDidOpen];
+}
+
+- (void)handleStreamClose:(NSError *_Nullable)error {
+ [super handleStreamClose:error];
+
+ [self.delegate writeStreamDidClose:error];
+}
+
+@end
+
+#pragma mark - FSTBetaWriteStream
+
+@implementation FSTBetaWriteStream {
+ FSTSerializerBeta *_serializer;
+}
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ serializer:(FSTSerializerBeta *)serializer
+ delegate:(id<FSTWriteStreamDelegate>)delegate {
+ self = [super initWithDatabase:database
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials
+ responseMessageClass:[GCFSWriteResponse class]
+ delegate:delegate];
+ if (self) {
+ _serializer = serializer;
+ }
+ return self;
+}
+
+- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter {
+ return [[GRPCCall alloc] initWithHost:self.databaseInfo.host
+ path:@"/google.firestore.v1beta1.Firestore/Write"
+ requestsWriter:requestsWriter];
+}
+
+- (void)writeHandshake {
+ // The initial request cannot contain mutations, but must contain a projectID.
+ FSTAssert([self isOpen], @"Not yet open");
+ FSTAssert(!self.handshakeComplete, @"Handshake sent out of turn");
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ GCFSWriteRequest *request = [GCFSWriteRequest message];
+ request.database = [_serializer encodedDatabaseID];
+ // TODO(dimond): Support stream resumption. We intentionally do not set the stream token on the
+ // handshake, ignoring any stream token we might have.
+
+ FSTLog(@"FSTWriteStream %p initial request: %@", (__bridge void *)self, request);
+ [self writeRequest:request];
+}
+
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
+ FSTAssert([self isOpen], @"Not yet open");
+ FSTAssert(self.handshakeComplete, @"Mutations sent out of turn");
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ NSMutableArray<GCFSWrite *> *protos = [NSMutableArray arrayWithCapacity:mutations.count];
+ for (FSTMutation *mutation in mutations) {
+ [protos addObject:[_serializer encodedMutation:mutation]];
+ };
+
+ GCFSWriteRequest *request = [GCFSWriteRequest message];
+ request.writesArray = protos;
+ request.streamToken = self.lastStreamToken;
+
+ FSTLog(@"FSTWriteStream %p mutation request: %@", (__bridge void *)self, request);
+ [self writeRequest:request];
+}
+
+/**
+ * Implements GRXWriteable to receive an inbound message from GRPC, deserialize, and then pass
+ * that on to the mutationResultsHandler.
+ */
+- (void)handleStreamMessage:(GCFSWriteResponse *)response {
+ FSTLog(@"FSTWriteStream %p response: %@", (__bridge void *)self, response);
+ [self.workerDispatchQueue verifyIsCurrentQueue];
+
+ // A successful response means the stream is healthy.
+ [self.backoff reset];
+
+ // Always capture the last stream token.
+ self.lastStreamToken = response.streamToken;
+
+ if (!self.handshakeComplete) {
+ // The first response is the handshake response
+ self.handshakeComplete = YES;
+
+ [self.delegate writeStreamDidCompleteHandshake];
+ } else {
+ FSTSnapshotVersion *commitVersion = [_serializer decodedVersion:response.commitTime];
+ NSMutableArray<GCFSWriteResult *> *protos = response.writeResultsArray;
+ NSMutableArray<FSTMutationResult *> *results = [NSMutableArray arrayWithCapacity:protos.count];
+ for (GCFSWriteResult *proto in protos) {
+ [results addObject:[_serializer decodedMutationResult:proto]];
+ };
+
+ [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results];
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTExistenceFilter.h b/Firestore/Source/Remote/FSTExistenceFilter.h
new file mode 100644
index 0000000..df95950
--- /dev/null
+++ b/Firestore/Source/Remote/FSTExistenceFilter.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTExistenceFilter : NSObject
+
++ (instancetype)filterWithCount:(int32_t)count;
+
+- (instancetype)init __attribute__((unavailable("Use a static constructor")));
+
+@property(nonatomic, assign, readonly) int32_t count;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTExistenceFilter.m b/Firestore/Source/Remote/FSTExistenceFilter.m
new file mode 100644
index 0000000..7c0ded2
--- /dev/null
+++ b/Firestore/Source/Remote/FSTExistenceFilter.m
@@ -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 "FSTExistenceFilter.h"
+
+@interface FSTExistenceFilter ()
+
+- (instancetype)initWithCount:(int32_t)count NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FSTExistenceFilter
+
++ (instancetype)filterWithCount:(int32_t)count {
+ return [[FSTExistenceFilter alloc] initWithCount:count];
+}
+
+- (instancetype)initWithCount:(int32_t)count {
+ if (self = [super init]) {
+ _count = count;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isMemberOfClass:[FSTExistenceFilter class]]) {
+ return NO;
+ }
+
+ return _count == ((FSTExistenceFilter *)other).count;
+}
+
+- (NSUInteger)hash {
+ return _count;
+}
+
+@end
diff --git a/Firestore/Source/Remote/FSTExponentialBackoff.h b/Firestore/Source/Remote/FSTExponentialBackoff.h
new file mode 100644
index 0000000..0bee2bd
--- /dev/null
+++ b/Firestore/Source/Remote/FSTExponentialBackoff.h
@@ -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 <Foundation/Foundation.h>
+
+@class FSTDispatchQueue;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Helper to implement exponential backoff.
+ *
+ * In general, call -reset after each successful round-trip. Call -backoffAndRunBlock before
+ * retrying after an error. Each backoffAndRunBlock will increase the delay between retries.
+ */
+@interface FSTExponentialBackoff : NSObject
+
+/**
+ * Creates and returns a helper for running delayed tasks following an exponential backoff curve
+ * between attempts.
+ *
+ * Each delay is made up of a "base" delay which follows the exponential backoff curve, and a
+ * +/- 50% "jitter" that is calculated and added to the base delay. This prevents clients from
+ * accidentally synchronizing their delays causing spikes of load to the backend.
+ *
+ * @param dispatchQueue The dispatch queue to run tasks on.
+ * @param initialDelay The initial delay (used as the base delay on the first retry attempt).
+ * Note that jitter will still be applied, so the actual delay could be as little as
+ * 0.5*initialDelay.
+ * @param backoffFactor The multiplier to use to determine the extended base delay after each
+ * attempt.
+ * @param maxDelay The maximum base delay after which no further backoff is performed. Note that
+ * jitter will still be applied, so the actual delay could be as much as 1.5*maxDelay.
+ */
++ (instancetype)exponentialBackoffWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue
+ initialDelay:(NSTimeInterval)initialDelay
+ backoffFactor:(double)backoffFactor
+ maxDelay:(NSTimeInterval)maxDelay;
+
+- (instancetype)init
+ __attribute__((unavailable("Use exponentialBackoffWithDispatchQueue constructor method.")));
+
+/**
+ * Resets the backoff delay.
+ *
+ * The very next backoffAndRunBlock: will have no delay. If it is called again (i.e. due to an
+ * error), initialDelay (plus jitter) will be used, and subsequent ones will increase according
+ * to the backoffFactor.
+ */
+- (void)reset;
+
+/**
+ * Resets the backoff to the maximum delay (e.g. for use after a RESOURCE_EXHAUSTED error).
+ */
+- (void)resetToMax;
+
+/**
+ * Waits for currentDelay seconds, increases the delay and runs the specified block.
+ *
+ * @param block The block to run.
+ */
+- (void)backoffAndRunBlock:(void (^)())block;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTExponentialBackoff.m b/Firestore/Source/Remote/FSTExponentialBackoff.m
new file mode 100644
index 0000000..ec21282
--- /dev/null
+++ b/Firestore/Source/Remote/FSTExponentialBackoff.m
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTExponentialBackoff.h"
+
+#import "FSTDispatchQueue.h"
+#import "FSTLogger.h"
+#import "FSTUtil.h"
+
+@interface FSTExponentialBackoff ()
+- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue
+ initialDelay:(NSTimeInterval)initialDelay
+ backoffFactor:(double)backoffFactor
+ maxDelay:(NSTimeInterval)maxDelay NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong) FSTDispatchQueue *dispatchQueue;
+@property(nonatomic) double backoffFactor;
+@property(nonatomic) NSTimeInterval initialDelay;
+@property(nonatomic) NSTimeInterval maxDelay;
+@property(nonatomic) NSTimeInterval currentBase;
+@end
+
+@implementation FSTExponentialBackoff
+
+- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue
+ initialDelay:(NSTimeInterval)initialDelay
+ backoffFactor:(double)backoffFactor
+ maxDelay:(NSTimeInterval)maxDelay {
+ if (self = [super init]) {
+ _dispatchQueue = dispatchQueue;
+ _initialDelay = initialDelay;
+ _backoffFactor = backoffFactor;
+ _maxDelay = maxDelay;
+
+ [self reset];
+ }
+ return self;
+}
+
++ (instancetype)exponentialBackoffWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue
+ initialDelay:(NSTimeInterval)initialDelay
+ backoffFactor:(double)backoffFactor
+ maxDelay:(NSTimeInterval)maxDelay {
+ return [[FSTExponentialBackoff alloc] initWithDispatchQueue:dispatchQueue
+ initialDelay:initialDelay
+ backoffFactor:backoffFactor
+ maxDelay:maxDelay];
+}
+
+- (void)reset {
+ _currentBase = 0;
+}
+
+- (void)resetToMax {
+ _currentBase = _maxDelay;
+}
+
+- (void)backoffAndRunBlock:(void (^)())block {
+ // First schedule the block using the current base (which may be 0 and should be honored as such).
+ NSTimeInterval delayWithJitter = _currentBase + [self jitterDelay];
+ if (_currentBase > 0) {
+ FSTLog(@"Backing off for %.2f seconds (base delay: %.2f seconds)", delayWithJitter,
+ _currentBase);
+ }
+ dispatch_time_t delay =
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayWithJitter * NSEC_PER_SEC));
+ dispatch_after(delay, self.dispatchQueue.queue, block);
+
+ // Apply backoff factor to determine next delay and ensure it is within bounds.
+ _currentBase *= _backoffFactor;
+ if (_currentBase < _initialDelay) {
+ _currentBase = _initialDelay;
+ }
+ if (_currentBase > _maxDelay) {
+ _currentBase = _maxDelay;
+ }
+}
+
+/** Returns a random value in the range [-currentBase/2, currentBase/2] */
+- (NSTimeInterval)jitterDelay {
+ return ([FSTUtil randomDouble] - 0.5) * _currentBase;
+}
+
+@end
diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h
new file mode 100644
index 0000000..939a027
--- /dev/null
+++ b/Firestore/Source/Remote/FSTRemoteEvent.h
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTDocumentDictionary.h"
+#import "FSTDocumentKeySet.h"
+#import "FSTTypes.h"
+
+@class FSTDocument;
+@class FSTDocumentKey;
+@class FSTExistenceFilter;
+@class FSTMaybeDocument;
+@class FSTSnapshotVersion;
+@class FSTWatchChange;
+@class FSTQueryData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTTargetMapping
+
+/**
+ * TargetMapping represents a change to the documents in a query from the server. This can either
+ * be an incremental Update or a full Reset.
+ *
+ * <p>This is an empty abstract class so that all the different kinds of changes can have a common
+ * base class.
+ */
+@interface FSTTargetMapping : NSObject
+@end
+
+#pragma mark - FSTResetMapping
+
+/** The new set of documents to replace the current documents for a target. */
+@interface FSTResetMapping : FSTTargetMapping
+
+/**
+ * Creates a new mapping with the keys for the given documents added. This is intended primarily
+ * for testing.
+ */
++ (FSTResetMapping *)mappingWithDocuments:(NSArray<FSTDocument *> *)documents;
+
+/** The new set of documents for the target. */
+@property(nonatomic, strong, readonly) FSTDocumentKeySet *documents;
+@end
+
+#pragma mark - FSTUpdateMapping
+
+/**
+ * A target should update its set of documents with the given added/removed set of documents.
+ */
+@interface FSTUpdateMapping : FSTTargetMapping
+
+/**
+ * Creates a new mapping with the keys for the given documents added. This is intended primarily
+ * for testing.
+ */
++ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added
+ removedDocuments:(NSArray<FSTDocument *> *)removed;
+
+- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys;
+
+/** The documents added to the target. */
+@property(nonatomic, strong, readonly) FSTDocumentKeySet *addedDocuments;
+/** The documents removed from the target. */
+@property(nonatomic, strong, readonly) FSTDocumentKeySet *removedDocuments;
+@end
+
+#pragma mark - FSTTargetChange
+
+/**
+ * Represents an update to the current status of a target, either explicitly having no new state, or
+ * the new value to set. Note "current" has special meaning in the RPC protocol that implies that a
+ * target is both up-to-date and consistent with the rest of the watch stream.
+ */
+typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) {
+ /** The current status is not affected and should not be modified */
+ FSTCurrentStatusUpdateNone,
+ /** The target must be marked as no longer "current" */
+ FSTCurrentStatusUpdateMarkNotCurrent,
+ /** The target must be marked as "current" */
+ FSTCurrentStatusUpdateMarkCurrent,
+};
+
+/**
+ * A part of an FSTRemoteEvent specifying set of changes to a specific target. These changes track
+ * what documents are currently included in the target as well as the current snapshot version and
+ * resume token but the actual changes *to* documents are not part of the FSTTargetChange since
+ * documents may be part of multiple targets.
+ */
+@interface FSTTargetChange : NSObject
+
+/**
+ * Creates a new target change with the given documents. Instances of FSTDocument are considered
+ * added. Instance of FSTDeletedDocument are considered removed. This is intended primarily for
+ * testing.
+ */
++ (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs
+ currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate;
+
+/**
+ * The new "current" (synced) status of this target. Set to CurrentStatusUpdateNone if the status
+ * should not be updated. Note "current" has special meaning for in the RPC protocol that implies
+ * that a target is both up-to-date and consistent with the rest of the watch stream.
+ */
+@property(nonatomic, assign, readonly) FSTCurrentStatusUpdate currentStatusUpdate;
+
+/** A set of changes to documents in this target. */
+@property(nonatomic, strong, readonly) FSTTargetMapping *mapping;
+
+/**
+ * The snapshot version representing the last state at which this target received a consistent
+ * snapshot from the backend.
+ */
+@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion;
+
+/**
+ * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting
+ * without retransmitting all the data that matches the query. The resume token essentially
+ * identifies a point in time from which the server should resume sending results.
+ */
+@property(nonatomic, strong, readonly) NSData *resumeToken;
+
+@end
+
+#pragma mark - FSTRemoteEvent
+
+/**
+ * An event from the RemoteStore. It is split into targetChanges (changes to the state or the set
+ * of documents in our watched targets) and documentUpdates (changes to the actual documents).
+ */
+@interface FSTRemoteEvent : NSObject
+
++ (instancetype)
+eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges
+ documentUpdates:
+ (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates;
+
+/** The snapshot version this event brings us up to. */
+@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion;
+
+/** A map from target to changes to the target. See TargetChange. */
+@property(nonatomic, strong, readonly)
+ NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges;
+
+/**
+ * A set of which documents have changed or been deleted, along with the doc's new values
+ * (if not deleted).
+ */
+@property(nonatomic, strong, readonly)
+ NSDictionary<FSTDocumentKey *, FSTMaybeDocument *> *documentUpdates;
+
+/** Adds a document update to this remote event */
+- (void)addDocumentUpdate:(FSTMaybeDocument *)document;
+
+/** Handles an existence filter mismatch */
+- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID;
+
+@end
+
+#pragma mark - FSTWatchChangeAggregator
+
+/**
+ * A helper class to accumulate watch changes into a FSTRemoteEvent and other target
+ * information.
+ */
+@interface FSTWatchChangeAggregator : NSObject
+
+- (instancetype)
+initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets
+ pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses
+ NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** The number of pending responses that are being waited on from watch */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingTargetResponses;
+
+/** Aggregates a watch change into the current state */
+- (void)addWatchChange:(FSTWatchChange *)watchChange;
+
+/** Aggregates all provided watch changes to the current state in order */
+- (void)addWatchChanges:(NSArray<FSTWatchChange *> *)watchChanges;
+
+/**
+ * Converts the current state into a remote event with the snapshot version taken from the
+ * initializer.
+ */
+- (FSTRemoteEvent *)remoteEvent;
+
+/** The existence filters - if any - for the given target IDs. */
+@property(nonatomic, strong, readonly)
+ NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *existenceFilters;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTRemoteEvent.m b/Firestore/Source/Remote/FSTRemoteEvent.m
new file mode 100644
index 0000000..5c75998
--- /dev/null
+++ b/Firestore/Source/Remote/FSTRemoteEvent.m
@@ -0,0 +1,516 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTRemoteEvent.h"
+
+#import "FSTAssert.h"
+#import "FSTClasses.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTLogger.h"
+#import "FSTSnapshotVersion.h"
+#import "FSTWatchChange.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTTargetMapping
+
+@interface FSTTargetMapping ()
+
+/** Private mutator method to add a document key to the mapping */
+- (void)addDocumentKey:(FSTDocumentKey *)documentKey;
+
+/** Private mutator method to remove a document key from the mapping */
+- (void)removeDocumentKey:(FSTDocumentKey *)documentKey;
+
+@end
+
+@implementation FSTTargetMapping
+
+- (void)addDocumentKey:(FSTDocumentKey *)documentKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)removeDocumentKey:(FSTDocumentKey *)documentKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+@end
+
+#pragma mark - FSTResetMapping
+
+@interface FSTResetMapping ()
+@property(nonatomic, strong) FSTDocumentKeySet *documents;
+@end
+
+@implementation FSTResetMapping
+
++ (instancetype)mappingWithDocuments:(NSArray<FSTDocument *> *)documents {
+ FSTResetMapping *mapping = [[FSTResetMapping alloc] init];
+ for (FSTDocument *doc in documents) {
+ mapping.documents = [mapping.documents setByAddingObject:doc.key];
+ }
+ return mapping;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _documents = [FSTDocumentKeySet keySet];
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isMemberOfClass:[FSTResetMapping class]]) {
+ return NO;
+ }
+
+ FSTResetMapping *otherMapping = (FSTResetMapping *)other;
+ return [self.documents isEqual:otherMapping.documents];
+}
+
+- (NSUInteger)hash {
+ return self.documents.hash;
+}
+
+- (void)addDocumentKey:(FSTDocumentKey *)documentKey {
+ self.documents = [self.documents setByAddingObject:documentKey];
+}
+
+- (void)removeDocumentKey:(FSTDocumentKey *)documentKey {
+ self.documents = [self.documents setByRemovingObject:documentKey];
+}
+
+@end
+
+#pragma mark - FSTUpdateMapping
+
+@interface FSTUpdateMapping ()
+@property(nonatomic, strong) FSTDocumentKeySet *addedDocuments;
+@property(nonatomic, strong) FSTDocumentKeySet *removedDocuments;
+@end
+
+@implementation FSTUpdateMapping
+
++ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added
+ removedDocuments:(NSArray<FSTDocument *> *)removed {
+ FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init];
+ for (FSTDocument *doc in added) {
+ mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key];
+ }
+ for (FSTDocument *doc in removed) {
+ mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key];
+ }
+ return mapping;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _addedDocuments = [FSTDocumentKeySet keySet];
+ _removedDocuments = [FSTDocumentKeySet keySet];
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isMemberOfClass:[FSTUpdateMapping class]]) {
+ return NO;
+ }
+
+ FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other;
+ return [self.addedDocuments isEqual:otherMapping.addedDocuments] &&
+ [self.removedDocuments isEqual:otherMapping.removedDocuments];
+}
+
+- (NSUInteger)hash {
+ return self.addedDocuments.hash * 31 + self.removedDocuments.hash;
+}
+
+- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys {
+ __block FSTDocumentKeySet *result = keys;
+ [self.addedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ result = [result setByAddingObject:key];
+ }];
+ [self.removedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ result = [result setByRemovingObject:key];
+ }];
+ return result;
+}
+
+- (void)addDocumentKey:(FSTDocumentKey *)documentKey {
+ self.addedDocuments = [self.addedDocuments setByAddingObject:documentKey];
+ self.removedDocuments = [self.removedDocuments setByRemovingObject:documentKey];
+}
+
+- (void)removeDocumentKey:(FSTDocumentKey *)documentKey {
+ self.addedDocuments = [self.addedDocuments setByRemovingObject:documentKey];
+ self.removedDocuments = [self.removedDocuments setByAddingObject:documentKey];
+}
+
+@end
+
+#pragma mark - FSTTargetChange
+
+@interface FSTTargetChange ()
+@property(nonatomic, assign) FSTCurrentStatusUpdate currentStatusUpdate;
+@property(nonatomic, strong, nullable) FSTTargetMapping *mapping;
+@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion;
+@property(nonatomic, strong) NSData *resumeToken;
+@end
+
+@implementation FSTTargetChange
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _currentStatusUpdate = FSTCurrentStatusUpdateNone;
+ _resumeToken = [NSData data];
+ }
+ return self;
+}
+
++ (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs
+ currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate {
+ FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init];
+ for (FSTMaybeDocument *doc in docs) {
+ if ([doc isKindOfClass:[FSTDeletedDocument class]]) {
+ mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key];
+ } else {
+ mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key];
+ }
+ }
+ FSTTargetChange *change = [[FSTTargetChange alloc] init];
+ change.mapping = mapping;
+ change.currentStatusUpdate = currentStatusUpdate;
+ return change;
+}
+
++ (instancetype)changeWithMapping:(FSTTargetMapping *)mapping
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate {
+ FSTTargetChange *change = [[FSTTargetChange alloc] init];
+ change.mapping = mapping;
+ change.snapshotVersion = snapshotVersion;
+ change.currentStatusUpdate = currentStatusUpdate;
+ return change;
+}
+
+- (FSTTargetMapping *)mapping {
+ if (!_mapping) {
+ // Create an FSTUpdateMapping by default, since resets are always explicit
+ _mapping = [[FSTUpdateMapping alloc] init];
+ }
+ return _mapping;
+}
+
+/**
+ * Sets the resume token but only when it has a new value. Empty resumeTokens are
+ * discarded.
+ */
+- (void)setResumeToken:(NSData *)resumeToken {
+ if (resumeToken.length > 0) {
+ _resumeToken = resumeToken;
+ }
+}
+
+@end
+
+#pragma mark - FSTRemoteEvent
+
+@interface FSTRemoteEvent () {
+ NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *_documentUpdates;
+ NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *_targetChanges;
+}
+
+- (instancetype)
+initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ targetChanges:(NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges
+ documentUpdates:
+ (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates;
+
+@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion;
+
+@end
+
+@implementation FSTRemoteEvent
+
++ (instancetype)
+eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges
+ documentUpdates:
+ (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates {
+ return [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion
+ targetChanges:targetChanges
+ documentUpdates:documentUpdates];
+}
+
+- (instancetype)
+initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges
+ documentUpdates:
+ (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates {
+ self = [super init];
+ if (self) {
+ _snapshotVersion = snapshotVersion;
+ _targetChanges = targetChanges;
+ _documentUpdates = documentUpdates;
+ }
+ return self;
+}
+
+/** Adds a document update to this remote event */
+- (void)addDocumentUpdate:(FSTMaybeDocument *)document {
+ _documentUpdates[document.key] = document;
+}
+
+/** Handles an existence filter mismatch */
+- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID {
+ // An existence filter mismatch will reset the query and we need to reset the mapping to contain
+ // no documents and an empty resume token.
+ //
+ // Note:
+ // * The reset mapping is empty, specifically forcing the consumer of the change to
+ // forget all keys for this targetID;
+ // * The resume snapshot for this target must be reset
+ // * The target must be unacked because unwatching and rewatching introduces a race for
+ // changes.
+ //
+ // TODO(dimond): keep track of reset targets not to raise.
+ FSTTargetChange *targetChange =
+ [FSTTargetChange changeWithMapping:[[FSTResetMapping alloc] init]
+ snapshotVersion:[FSTSnapshotVersion noVersion]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkNotCurrent];
+ _targetChanges[targetID] = targetChange;
+}
+
+@end
+
+#pragma mark - FSTWatchChangeAggregator
+
+@interface FSTWatchChangeAggregator ()
+
+/** The snapshot version for every target change this creates. */
+@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion;
+
+/** Keeps track of the current target mappings */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges;
+
+/** Keeps track of document to update */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *documentUpdates;
+
+/** The set of open listens on the client */
+@property(nonatomic, strong, readonly)
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *listenTargets;
+
+/** Whether this aggregator was frozen and can no longer be modified */
+@property(nonatomic, assign) BOOL frozen;
+
+@end
+
+@implementation FSTWatchChangeAggregator {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *_existenceFilters;
+}
+
+- (instancetype)
+initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets
+ pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses {
+ self = [super init];
+ if (self) {
+ _snapshotVersion = snapshotVersion;
+
+ _frozen = NO;
+ _targetChanges = [NSMutableDictionary dictionary];
+ _listenTargets = listenTargets;
+ _pendingTargetResponses = [NSMutableDictionary dictionaryWithDictionary:pendingTargetResponses];
+
+ _existenceFilters = [NSMutableDictionary dictionary];
+ _documentUpdates = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+- (FSTTargetChange *)targetChangeForTargetID:(FSTBoxedTargetID *)targetID {
+ FSTTargetChange *change = self.targetChanges[targetID];
+ if (!change) {
+ change = [[FSTTargetChange alloc] init];
+ change.snapshotVersion = self.snapshotVersion;
+ self.targetChanges[targetID] = change;
+ }
+ return change;
+}
+
+- (void)addWatchChanges:(NSArray<FSTWatchChange *> *)watchChanges {
+ FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator");
+ for (FSTWatchChange *watchChange in watchChanges) {
+ [self addWatchChange:watchChange];
+ }
+}
+
+- (void)addWatchChange:(FSTWatchChange *)watchChange {
+ FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator");
+ if ([watchChange isKindOfClass:[FSTDocumentWatchChange class]]) {
+ [self addDocumentChange:(FSTDocumentWatchChange *)watchChange];
+ } else if ([watchChange isKindOfClass:[FSTWatchTargetChange class]]) {
+ [self addTargetChange:(FSTWatchTargetChange *)watchChange];
+ } else if ([watchChange isKindOfClass:[FSTExistenceFilterWatchChange class]]) {
+ [self addExistenceFilterChange:(FSTExistenceFilterWatchChange *)watchChange];
+ } else {
+ FSTFail(@"Unknown watch change: %@", watchChange);
+ }
+}
+
+- (void)addDocumentChange:(FSTDocumentWatchChange *)docChange {
+ BOOL relevant = NO;
+
+ for (FSTBoxedTargetID *targetID in docChange.updatedTargetIDs) {
+ if ([self isActiveTarget:targetID]) {
+ FSTTargetChange *change = [self targetChangeForTargetID:targetID];
+ [change.mapping addDocumentKey:docChange.documentKey];
+ relevant = YES;
+ }
+ }
+
+ for (FSTBoxedTargetID *targetID in docChange.removedTargetIDs) {
+ if ([self isActiveTarget:targetID]) {
+ FSTTargetChange *change = [self targetChangeForTargetID:targetID];
+ [change.mapping removeDocumentKey:docChange.documentKey];
+ relevant = YES;
+ }
+ }
+
+ // Only update the document if there is a new document to replace, this might be just a target
+ // update instead.
+ if (docChange.document && relevant) {
+ self.documentUpdates[docChange.documentKey] = docChange.document;
+ }
+}
+
+- (void)addTargetChange:(FSTWatchTargetChange *)targetChange {
+ for (FSTBoxedTargetID *targetID in targetChange.targetIDs) {
+ FSTTargetChange *change = [self targetChangeForTargetID:targetID];
+ switch (targetChange.state) {
+ case FSTWatchTargetChangeStateNoChange:
+ if ([self isActiveTarget:targetID]) {
+ // Creating the change above satisfies the semantics of no-change.
+ change.resumeToken = targetChange.resumeToken;
+ }
+ break;
+ case FSTWatchTargetChangeStateAdded:
+ [self recordResponseForTargetID:targetID];
+ if (![self.pendingTargetResponses objectForKey:targetID]) {
+ // We have a freshly added target, so we need to reset any state that we had previously
+ // This can happen e.g. when remove and add back a target for existence filter
+ // mismatches.
+ change.mapping = nil;
+ change.currentStatusUpdate = FSTCurrentStatusUpdateNone;
+ [_existenceFilters removeObjectForKey:targetID];
+ }
+ change.resumeToken = targetChange.resumeToken;
+ break;
+ case FSTWatchTargetChangeStateRemoved:
+ // We need to keep track of removed targets to we can post-filter and remove any target
+ // changes.
+ [self recordResponseForTargetID:targetID];
+ FSTAssert(!targetChange.cause, @"WatchChangeAggregator does not handle errored targets.");
+ break;
+ case FSTWatchTargetChangeStateCurrent:
+ if ([self isActiveTarget:targetID]) {
+ change.currentStatusUpdate = FSTCurrentStatusUpdateMarkCurrent;
+ change.resumeToken = targetChange.resumeToken;
+ }
+ break;
+ case FSTWatchTargetChangeStateReset:
+ if ([self isActiveTarget:targetID]) {
+ // Overwrite any existing target mapping with a reset mapping. Every subsequent update
+ // will modify the reset mapping, not an update mapping.
+ change.mapping = [[FSTResetMapping alloc] init];
+ change.resumeToken = targetChange.resumeToken;
+ }
+ break;
+ default:
+ FSTWarn(@"Unknown target watch change type: %ld", (long)targetChange.state);
+ }
+ }
+}
+
+/**
+ * Records that we got a watch target add/remove by decrementing the number of pending target
+ * responses that we have.
+ */
+- (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID {
+ NSNumber *count = [self.pendingTargetResponses objectForKey:targetID];
+ int newCount = count ? [count intValue] - 1 : -1;
+ if (newCount == 0) {
+ [self.pendingTargetResponses removeObjectForKey:targetID];
+ } else {
+ [self.pendingTargetResponses setObject:[NSNumber numberWithInt:newCount] forKey:targetID];
+ }
+}
+
+/**
+ * Returns true if the given targetId is active. Active targets are those for which there are no
+ * pending requests to add a listen and are in the current list of targets the client cares about.
+ *
+ * Clients can repeatedly listen and stop listening to targets, so this check is useful in
+ * preventing in preventing race conditions for a target where events arrive but the server hasn't
+ * yet acknowledged the intended change in state.
+ */
+- (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID {
+ return [self.listenTargets objectForKey:targetID] &&
+ ![self.pendingTargetResponses objectForKey:targetID];
+}
+
+- (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange {
+ FSTBoxedTargetID *targetID = @(existenceFilterChange.targetID);
+ if ([self isActiveTarget:targetID]) {
+ _existenceFilters[targetID] = existenceFilterChange.filter;
+ }
+}
+
+- (FSTRemoteEvent *)remoteEvent {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges = self.targetChanges;
+
+ NSMutableArray *targetsToRemove = [NSMutableArray array];
+
+ // Apply any inactive targets.
+ for (FSTBoxedTargetID *targetID in [targetChanges keyEnumerator]) {
+ if (![self isActiveTarget:targetID]) {
+ [targetsToRemove addObject:targetID];
+ }
+ }
+
+ [targetChanges removeObjectsForKeys:targetsToRemove];
+
+ // Mark this aggregator as frozen so no further modifications are made.
+ self.frozen = YES;
+ return [FSTRemoteEvent eventWithSnapshotVersion:self.snapshotVersion
+ targetChanges:targetChanges
+ documentUpdates:self.documentUpdates];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h
new file mode 100644
index 0000000..94208e1
--- /dev/null
+++ b/Firestore/Source/Remote/FSTRemoteStore.h
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTDocumentVersionDictionary.h"
+#import "FSTTypes.h"
+
+@class FSTDatabaseInfo;
+@class FSTDatastore;
+@class FSTDocumentKey;
+@class FSTLocalStore;
+@class FSTMutationBatch;
+@class FSTMutationBatchResult;
+@class FSTQuery;
+@class FSTQueryData;
+@class FSTRemoteEvent;
+@class FSTTransaction;
+@class FSTUser;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTRemoteSyncer
+
+/**
+ * A protocol that describes the actions the FSTRemoteStore needs to perform on a cooperating
+ * synchronization engine.
+ */
+@protocol FSTRemoteSyncer
+
+/**
+ * Applies one remote event to the sync engine, notifying any views of the changes, and releasing
+ * any pending mutation batches that would become visible because of the snapshot version the
+ * remote event contains.
+ */
+- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent;
+
+/**
+ * Rejects the listen for the given targetID. This can be triggered by the backend for any active
+ * target.
+ *
+ * @param targetID The targetID corresponding to a listen initiated via
+ * -listenToTargetWithQueryData: on FSTRemoteStore.
+ * @param error A description of the condition that has forced the rejection. Nearly always this
+ * will be an indication that the user is no longer authorized to see the data matching the
+ * target.
+ */
+- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error;
+
+/**
+ * Applies the result of a successful write of a mutation batch to the sync engine, emitting
+ * snapshots in any views that the mutation applies to, and removing the batch from the mutation
+ * queue.
+ */
+- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult;
+
+/**
+ * Rejects the batch, removing the batch from the mutation queue, recomputing the local view of
+ * any documents affected by the batch and then, emitting snapshots with the reverted value.
+ */
+- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error;
+
+@end
+
+/**
+ * A protocol for the FSTRemoteStore online state delegate, called whenever the state of the
+ * online streams of the FSTRemoteStore changes.
+ * Note that this protocol only supports the watch stream for now.
+ */
+@protocol FSTOnlineStateDelegate <NSObject>
+
+/** Called whenever the online state of the watch stream changes */
+- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState;
+
+@end
+
+#pragma mark - FSTRemoteStore
+
+/**
+ * FSTRemoteStore handles all interaction with the backend through a simple, clean interface. This
+ * class is not thread safe and should be only called from the worker dispatch queue.
+ */
+@interface FSTRemoteStore : NSObject
+
++ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore
+ datastore:(FSTDatastore *)datastore;
+
+- (instancetype)init __attribute__((unavailable("Use static constructor method.")));
+
+@property(nonatomic, weak) id<FSTRemoteSyncer> syncEngine;
+
+@property(nonatomic, weak) id<FSTOnlineStateDelegate> onlineStateDelegate;
+
+/** Starts up the remote store, creating streams, restoring state from LocalStore, etc. */
+- (void)start;
+
+/** Shuts down the remote store, tearing down connections and otherwise cleaning up. */
+- (void)shutdown;
+
+/**
+ * Tells the FSTRemoteStore that the currently authenticated user has changed.
+ *
+ * In response the remote store tears down streams and clears up any tracked operations that should
+ * not persist across users. Restarts the streams if appropriate.
+ */
+- (void)userDidChange:(FSTUser *)user;
+
+/** Listens to the target identified by the given FSTQueryData. */
+- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData;
+
+/** Stops listening to the target with the given target ID. */
+- (void)stopListeningToTargetID:(FSTTargetID)targetID;
+
+/**
+ * Tells the FSTRemoteStore that there are new mutations to process in the queue. This is typically
+ * called by FSTSyncEngine after it has sent mutations to FSTLocalStore.
+ *
+ * In response the remote store will pull mutations from the local store until the datastore
+ * instance reports that it cannot accept further in-progress writes. This mechanism serves to
+ * maintain a pipeline of in-flight requests between the FSTDatastore and the server that
+ * applies them.
+ */
+- (void)fillWritePipeline;
+
+/** Returns a new transaction backed by this remote store. */
+- (FSTTransaction *)transaction;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTRemoteStore.m b/Firestore/Source/Remote/FSTRemoteStore.m
new file mode 100644
index 0000000..cea2ce8
--- /dev/null
+++ b/Firestore/Source/Remote/FSTRemoteStore.m
@@ -0,0 +1,599 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTRemoteStore.h"
+
+#import "FSTAssert.h"
+#import "FSTDatastore.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTExistenceFilter.h"
+#import "FSTLocalStore.h"
+#import "FSTLogger.h"
+#import "FSTMutation.h"
+#import "FSTMutationBatch.h"
+#import "FSTQuery.h"
+#import "FSTQueryData.h"
+#import "FSTRemoteEvent.h"
+#import "FSTSnapshotVersion.h"
+#import "FSTTransaction.h"
+#import "FSTWatchChange.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The maximum number of pending writes to allow.
+ * TODO(bjornick): Negotiate this value with the backend.
+ */
+static const NSUInteger kMaxPendingWrites = 10;
+
+#pragma mark - FSTRemoteStore
+
+@interface FSTRemoteStore () <FSTWatchStreamDelegate, FSTWriteStreamDelegate>
+
+- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore
+ datastore:(FSTDatastore *)datastore NS_DESIGNATED_INITIALIZER;
+
+/**
+ * The local store, used to fill the write pipeline with outbound mutations and resolve existence
+ * filter mismatches. Immutable after initialization.
+ */
+@property(nonatomic, strong, readonly) FSTLocalStore *localStore;
+
+/** The client-side proxy for interacting with the backend. Immutable after initialization. */
+@property(nonatomic, strong, readonly) FSTDatastore *datastore;
+
+#pragma mark Watch Stream
+@property(nonatomic, strong, nullable) FSTWatchStream *watchStream;
+
+/**
+ * A mapping of watched targets that the client cares about tracking and the
+ * user has explicitly called a 'listen' for this target.
+ *
+ * These targets may or may not have been sent to or acknowledged by the
+ * server. On re-establishing the listen stream, these targets should be sent
+ * to the server. The targets removed with unlistens are removed eagerly
+ * without waiting for confirmation from the listen stream. */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *listenTargets;
+
+/**
+ * A mapping of targetId to pending acks needed.
+ *
+ * If a targetId is present in this map, then we're waiting for watch to
+ * acknowledge a removal or addition of the target. If a target is not in this
+ * mapping, and it's in the listenTargets map, then we consider the target to
+ * be active.
+ *
+ * We increment the count here everytime we issue a request over the stream to
+ * watch or unwatch. We then decrement the count everytime we get a target
+ * added or target removed message from the server. Once the count is equal to
+ * 0 we know that the client and server are in the same state (once this state
+ * is reached the targetId is removed from the map to free the memory).
+ */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingTargetResponses;
+
+@property(nonatomic, strong) NSMutableArray<FSTWatchChange *> *accumulatedChanges;
+@property(nonatomic, assign) FSTBatchID lastBatchSeen;
+
+/**
+ * The online state of the watch stream. The state is set to healthy if and only if there are
+ * messages received by the backend.
+ */
+@property(nonatomic, assign) FSTOnlineState watchStreamOnlineState;
+
+#pragma mark Write Stream
+@property(nonatomic, strong, nullable) FSTWriteStream *writeStream;
+
+/**
+ * The approximate time the StreamingWrite stream was opened. Used to estimate if stream was
+ * closed due to an auth expiration (a recoverable error) or some other more permanent error.
+ */
+@property(nonatomic, strong, nullable) NSDate *writeStreamOpenTime;
+
+/**
+ * A FIFO queue of in-flight writes. This is in-flight from the point of view of the caller of
+ * writeMutations, not from the point of view from the Datastore itself. In particular, these
+ * requests may not have been sent to the Datastore server if the write stream is not yet running.
+ */
+@property(nonatomic, strong, readonly) NSMutableArray<FSTMutationBatch *> *pendingWrites;
+@end
+
+@implementation FSTRemoteStore
+
++ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore
+ datastore:(FSTDatastore *)datastore {
+ return [[FSTRemoteStore alloc] initWithLocalStore:localStore datastore:datastore];
+}
+
+- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore datastore:(FSTDatastore *)datastore {
+ if (self = [super init]) {
+ _localStore = localStore;
+ _datastore = datastore;
+ _listenTargets = [NSMutableDictionary dictionary];
+ _pendingTargetResponses = [NSMutableDictionary dictionary];
+ _accumulatedChanges = [NSMutableArray array];
+
+ _lastBatchSeen = kFSTBatchIDUnknown;
+ _watchStreamOnlineState = FSTOnlineStateUnknown;
+ _pendingWrites = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (void)start {
+ [self setupStreams];
+
+ // Resume any writes
+ [self fillWritePipeline];
+}
+
+- (void)updateAndNotifyAboutOnlineState:(FSTOnlineState)watchStreamOnlineState {
+ BOOL didChange = (watchStreamOnlineState != self.watchStreamOnlineState);
+ self.watchStreamOnlineState = watchStreamOnlineState;
+ if (didChange) {
+ [self.onlineStateDelegate watchStreamDidChangeOnlineState:watchStreamOnlineState];
+ }
+}
+
+- (void)setupStreams {
+ self.watchStream = [self.datastore createWatchStreamWithDelegate:self];
+ self.writeStream = [self.datastore createWriteStreamWithDelegate:self];
+
+ // Load any saved stream token from persistent storage
+ self.writeStream.lastStreamToken = [self.localStore lastStreamToken];
+}
+
+#pragma mark Shutdown
+
+- (void)shutdown {
+ FSTLog(@"FSTRemoteStore %p shutting down", (__bridge void *)self);
+
+ self.watchStreamOnlineState = FSTOnlineStateUnknown;
+ [self cleanupWatchStreamState];
+ [self.watchStream stop];
+ [self.writeStream stop];
+}
+
+- (void)userDidChange:(FSTUser *)user {
+ FSTLog(@"FSTRemoteStore %p changing users: %@", (__bridge void *)self, user);
+
+ // Clear pending writes because those are per-user. Watched targets persist across users so
+ // don't clear those.
+ _lastBatchSeen = kFSTBatchIDUnknown;
+ [self.pendingWrites removeAllObjects];
+
+ // Stop the streams. They promise not to call us back.
+ [self.watchStream stop];
+ [self.writeStream stop];
+
+ [self cleanupWatchStreamState];
+
+ // Create new streams (but note they're not started yet).
+ [self setupStreams];
+
+ // If there are any watchedTargets properly handle the stream restart now that FSTRemoteStore
+ // is ready to handle them.
+ if ([self shouldStartWatchStream]) {
+ [self.watchStream start];
+ }
+
+ // Resume any writes
+ [self fillWritePipeline];
+
+ // User change moves us back to the unknown state because we might not
+ // want to re-open the stream
+ [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown];
+}
+
+#pragma mark Watch Stream
+
+- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData {
+ NSNumber *targetKey = @(queryData.targetID);
+ FSTAssert(!self.listenTargets[targetKey], @"listenToQuery called with duplicate target id: %@",
+ targetKey);
+
+ self.listenTargets[targetKey] = queryData;
+ if ([self.watchStream isOpen]) {
+ [self sendWatchRequestWithQueryData:queryData];
+ } else if (![self.watchStream isStarted]) {
+ [self.watchStream start];
+ }
+}
+
+- (void)sendWatchRequestWithQueryData:(FSTQueryData *)queryData {
+ [self recordPendingRequestForTargetID:@(queryData.targetID)];
+ [self.watchStream watchQuery:queryData];
+}
+
+- (void)stopListeningToTargetID:(FSTTargetID)targetID {
+ FSTBoxedTargetID *targetKey = @(targetID);
+ FSTQueryData *queryData = self.listenTargets[targetKey];
+ FSTAssert(queryData, @"unlistenToTarget: target not currently watched: %@", targetKey);
+
+ [self.listenTargets removeObjectForKey:targetKey];
+ if ([self.watchStream isOpen]) {
+ [self sendUnwatchRequestForTargetID:targetKey];
+ }
+}
+
+- (void)sendUnwatchRequestForTargetID:(FSTBoxedTargetID *)targetID {
+ [self recordPendingRequestForTargetID:targetID];
+ [self.watchStream unwatchTargetID:[targetID intValue]];
+}
+
+- (void)recordPendingRequestForTargetID:(FSTBoxedTargetID *)targetID {
+ NSNumber *count = [self.pendingTargetResponses objectForKey:targetID];
+ count = @([count intValue] + 1);
+ [self.pendingTargetResponses setObject:count forKey:targetID];
+}
+
+/**
+ * Returns whether the watch stream should be started because there are active targets trying to
+ * be listened to.
+ */
+- (BOOL)shouldStartWatchStream {
+ return self.listenTargets.count > 0;
+}
+
+- (void)cleanupWatchStreamState {
+ // If the connection is closed then we'll never get a snapshot version for the accumulated
+ // changes and so we'll never be able to complete the batch. When we start up again the server
+ // is going to resend these changes anyway, so just toss the accumulated state.
+ [self.accumulatedChanges removeAllObjects];
+ [self.pendingTargetResponses removeAllObjects];
+}
+
+- (void)watchStreamDidOpen {
+ // Restore any existing watches.
+ for (FSTQueryData *queryData in [self.listenTargets objectEnumerator]) {
+ [self sendWatchRequestWithQueryData:queryData];
+ }
+}
+
+- (void)watchStreamDidChange:(FSTWatchChange *)change
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion {
+ // Mark the connection as healthy because we got a message from the server.
+ [self updateAndNotifyAboutOnlineState:FSTOnlineStateHealthy];
+
+ FSTWatchTargetChange *watchTargetChange =
+ [change isKindOfClass:[FSTWatchTargetChange class]] ? (FSTWatchTargetChange *)change : nil;
+
+ if (watchTargetChange && watchTargetChange.state == FSTWatchTargetChangeStateRemoved &&
+ watchTargetChange.cause) {
+ // There was an error on a target, don't wait for a consistent snapshot to raise events
+ [self processTargetErrorForWatchChange:(FSTWatchTargetChange *)change];
+ } else {
+ // Accumulate watch changes but don't process them if there's no snapshotVersion or it's
+ // older than a previous snapshot we've processed (can happen after we resume a target
+ // using a resume token).
+ [self.accumulatedChanges addObject:change];
+ FSTAssert(snapshotVersion, @"snapshotVersion must not be nil.");
+ if ([snapshotVersion isEqual:[FSTSnapshotVersion noVersion]] ||
+ [snapshotVersion compare:[self.localStore lastRemoteSnapshotVersion]] ==
+ NSOrderedAscending) {
+ return;
+ }
+
+ // Create a batch, giving it the accumulatedChanges array.
+ NSArray<FSTWatchChange *> *changes = self.accumulatedChanges;
+ self.accumulatedChanges = [NSMutableArray array];
+
+ [self processBatchedWatchChanges:changes snapshotVersion:snapshotVersion];
+ }
+}
+
+- (void)watchStreamDidClose:(NSError *_Nullable)error {
+ [self cleanupWatchStreamState];
+
+ // If there was an error, retry the connection.
+ if ([self shouldStartWatchStream]) {
+ // If the connection fails before the stream has become healthy, consider the online state
+ // failed. Otherwise consider the online state unknown and the next connection attempt will
+ // resolve the online state. For example, if a healthy stream is closed due to an expired token
+ // we want to have one more try at reconnecting before we consider the connection unhealthy.
+ if (self.watchStreamOnlineState == FSTOnlineStateHealthy) {
+ [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown];
+ } else {
+ [self updateAndNotifyAboutOnlineState:FSTOnlineStateFailed];
+ }
+ [self.watchStream start];
+ } else {
+ // No need to restart watch stream because there are no active targets. The online state is set
+ // to unknown because there is no active attempt at establishing a connection.
+ [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown];
+ }
+}
+
+/**
+ * Takes a batch of changes from the Datastore, repackages them as a RemoteEvent, and passes that
+ * on to the SyncEngine.
+ */
+- (void)processBatchedWatchChanges:(NSArray<FSTWatchChange *> *)changes
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion {
+ FSTWatchChangeAggregator *aggregator =
+ [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:snapshotVersion
+ listenTargets:self.listenTargets
+ pendingTargetResponses:self.pendingTargetResponses];
+ [aggregator addWatchChanges:changes];
+ FSTRemoteEvent *remoteEvent = [aggregator remoteEvent];
+ [self.pendingTargetResponses removeAllObjects];
+ [self.pendingTargetResponses setDictionary:aggregator.pendingTargetResponses];
+
+ // Handle existence filters and existence filter mismatches
+ [aggregator.existenceFilters enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *target,
+ FSTExistenceFilter *filter,
+ BOOL *stop) {
+ FSTTargetID targetID = target.intValue;
+
+ FSTQueryData *queryData = self.listenTargets[target];
+ FSTQuery *query = queryData.query;
+ if (!queryData) {
+ // A watched target might have been removed already.
+ return;
+
+ } else if ([query isDocumentQuery]) {
+ if (filter.count == 0) {
+ // The existence filter told us the document does not exist.
+ // We need to deduce that this document does not exist and apply a deleted document to our
+ // updates. Without applying a deleted document there might be another query that will
+ // raise this document as part of a snapshot until it is resolved, essentially exposing
+ // inconsistency between queries
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:query.path];
+ FSTDeletedDocument *deletedDoc =
+ [FSTDeletedDocument documentWithKey:key version:snapshotVersion];
+ [remoteEvent addDocumentUpdate:deletedDoc];
+ } else {
+ FSTAssert(filter.count == 1, @"Single document existence filter with count: %" PRId32,
+ filter.count);
+ }
+
+ } else {
+ // Not a document query.
+ FSTDocumentKeySet *trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID];
+ FSTTargetMapping *mapping = remoteEvent.targetChanges[target].mapping;
+ if (mapping) {
+ if ([mapping isKindOfClass:[FSTUpdateMapping class]]) {
+ FSTUpdateMapping *update = (FSTUpdateMapping *)mapping;
+ trackedRemote = [update applyTo:trackedRemote];
+ } else {
+ FSTAssert([mapping isKindOfClass:[FSTResetMapping class]],
+ @"Expected either reset or update mapping but got something else %@", mapping);
+ trackedRemote = ((FSTResetMapping *)mapping).documents;
+ }
+ }
+
+ if (trackedRemote.count != (NSUInteger)filter.count) {
+ FSTLog(@"Existence filter mismatch, resetting mapping");
+
+ // Make sure the mismatch is exposed in the remote event
+ [remoteEvent handleExistenceFilterMismatchForTargetID:target];
+
+ // Clear the resume token for the query, since we're in a known mismatch state.
+ queryData =
+ [[FSTQueryData alloc] initWithQuery:query targetID:targetID purpose:queryData.purpose];
+ self.listenTargets[target] = queryData;
+
+ // Cause a hard reset by unwatching and rewatching immediately, but deliberately don't
+ // send a resume token so that we get a full update.
+ [self sendUnwatchRequestForTargetID:@(targetID)];
+
+ // Mark the query we send as being on behalf of an existence filter mismatch, but don't
+ // actually retain that in listenTargets. This ensures that we flag the first re-listen
+ // this way without impacting future listens of this target (that might happen e.g. on
+ // reconnect).
+ FSTQueryData *requestQueryData =
+ [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID
+ purpose:FSTQueryPurposeExistenceFilterMismatch];
+ [self sendWatchRequestWithQueryData:requestQueryData];
+ }
+ }
+ }];
+
+ // Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when
+ // applying the completed FSTRemoteEvent.
+ [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^(
+ FSTBoxedTargetID *target, FSTTargetChange *change, BOOL *stop) {
+ NSData *resumeToken = change.resumeToken;
+ if (resumeToken.length > 0) {
+ FSTQueryData *queryData = _listenTargets[target];
+ // A watched target might have been removed already.
+ if (queryData) {
+ _listenTargets[target] =
+ [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion
+ resumeToken:resumeToken];
+ }
+ }
+ }];
+
+ // Finally handle remote event
+ [self.syncEngine applyRemoteEvent:remoteEvent];
+}
+
+/** Process a target error and passes the error along to SyncEngine. */
+- (void)processTargetErrorForWatchChange:(FSTWatchTargetChange *)change {
+ FSTAssert(change.cause, @"Handling target error without a cause");
+ // Ignore targets that have been removed already.
+ for (FSTBoxedTargetID *targetID in change.targetIDs) {
+ if (self.listenTargets[targetID]) {
+ [self.listenTargets removeObjectForKey:targetID];
+ [self.syncEngine rejectListenWithTargetID:targetID error:change.cause];
+ }
+ }
+}
+
+#pragma mark Write Stream
+
+- (void)fillWritePipeline {
+ while ([self canWriteMutations]) {
+ FSTMutationBatch *batch = [self.localStore nextMutationBatchAfterBatchID:self.lastBatchSeen];
+ if (!batch) {
+ break;
+ }
+ [self commitBatch:batch];
+ }
+}
+
+/**
+ * Returns YES if the backend can accept additional write requests.
+ *
+ * When sending mutations to the write stream (e.g. in -fillWritePipeline), call this method first
+ * to check if more mutations can be sent.
+ *
+ * Currently the only thing that can prevent the backend from accepting write requests is if
+ * there are too many requests already outstanding. As writes complete the backend will be able
+ * to accept more.
+ */
+- (BOOL)canWriteMutations {
+ return self.pendingWrites.count < kMaxPendingWrites;
+}
+
+/** Given mutations to commit, actually commits them to the backend. */
+- (void)commitBatch:(FSTMutationBatch *)batch {
+ FSTAssert([self canWriteMutations], @"commitBatch called when mutations can't be written");
+ self.lastBatchSeen = batch.batchID;
+
+ if (!self.writeStream.isStarted) {
+ [self.writeStream start];
+ }
+
+ [self.pendingWrites addObject:batch];
+
+ if (self.writeStream.handshakeComplete) {
+ [self.writeStream writeMutations:batch.mutations];
+ }
+}
+
+- (void)writeStreamDidOpen {
+ self.writeStreamOpenTime = [NSDate date];
+
+ [self.writeStream writeHandshake];
+}
+
+/**
+ * Handles a successful handshake response from the server, which is our cue to send any pending
+ * writes.
+ */
+- (void)writeStreamDidCompleteHandshake {
+ // Record the stream token.
+ [self.localStore setLastStreamToken:self.writeStream.lastStreamToken];
+
+ // Drain any pending writes.
+ //
+ // Note that at this point pendingWrites contains mutations that have already been accepted by
+ // fillWritePipeline/commitBatch. If the pipeline is full, canWriteMutations will be NO, despite
+ // the fact that we actually need to send mutations over.
+ //
+ // This also means that this method indirectly respects the limits imposed by canWriteMutations
+ // since writes can't be added to the pendingWrites array when canWriteMutations is NO. If the
+ // limits imposed by canWriteMutations actually protect us from DOSing ourselves then those limits
+ // won't be exceeded here and we'll continue to make progress.
+ for (FSTMutationBatch *write in self.pendingWrites) {
+ [self.writeStream writeMutations:write.mutations];
+ }
+}
+
+/** Handles a successful StreamingWriteResponse from the server that contains a mutation result. */
+- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)results {
+ // This is a response to a write containing mutations and should be correlated to the first
+ // pending write.
+ NSMutableArray *pendingWrites = self.pendingWrites;
+ FSTMutationBatch *batch = pendingWrites[0];
+ [pendingWrites removeObjectAtIndex:0];
+
+ FSTMutationBatchResult *batchResult =
+ [FSTMutationBatchResult resultWithBatch:batch
+ commitVersion:commitVersion
+ mutationResults:results
+ streamToken:self.writeStream.lastStreamToken];
+ [self.syncEngine applySuccessfulWriteWithResult:batchResult];
+
+ // It's possible that with the completion of this mutation another slot has freed up.
+ [self fillWritePipeline];
+}
+
+/**
+ * Handles the closing of the StreamingWrite RPC, either because of an error or because the RPC
+ * has been terminated by the client or the server.
+ */
+- (void)writeStreamDidClose:(NSError *_Nullable)error {
+ NSMutableArray *pendingWrites = self.pendingWrites;
+ // Ignore close if there are no pending writes.
+ if (pendingWrites.count == 0) {
+ return;
+ }
+
+ FSTAssert(error, @"There are pending writes, but the write stream closed without an error.");
+ if ([FSTDatastore isPermanentWriteError:error]) {
+ if (self.writeStream.handshakeComplete) {
+ // This error affects the actual writes.
+ [self handleWriteError:error];
+ } else {
+ // If there was an error before the handshake finished, it's possible that the server is
+ // unable to process the stream token we're sending. (Perhaps it's too old?)
+ [self handleHandshakeError:error];
+ }
+ }
+
+ // The write stream might have been started by refilling the write pipeline for failed writes
+ if (pendingWrites.count > 0 && !self.writeStream.isStarted) {
+ [self.writeStream start];
+ }
+}
+
+- (void)handleHandshakeError:(NSError *)error {
+ // Reset the token if it's a permanent error or the error code is ABORTED, signaling the write
+ // stream is no longer valid.
+ if ([FSTDatastore isPermanentWriteError:error] || [FSTDatastore isAbortedError:error]) {
+ NSString *token = [self.writeStream.lastStreamToken base64EncodedStringWithOptions:0];
+ FSTLog(@"FSTRemoteStore %p error before completed handshake; resetting stream token %@: %@",
+ (__bridge void *)self, token, error);
+ self.writeStream.lastStreamToken = nil;
+ [self.localStore setLastStreamToken:nil];
+ }
+}
+
+- (void)handleWriteError:(NSError *)error {
+ // Only handle permanent error. If it's transient, just let the retry logic kick in.
+ if (![FSTDatastore isPermanentWriteError:error]) {
+ return;
+ }
+
+ // If this was a permanent error, the request itself was the problem so it's not going to
+ // succeed if we resend it.
+ FSTMutationBatch *batch = self.pendingWrites[0];
+ [self.pendingWrites removeObjectAtIndex:0];
+
+ // In this case it's also unlikely that the server itself is melting down--this was just a
+ // bad request so inhibit backoff on the next restart.
+ [self.writeStream inhibitBackoff];
+
+ [self.syncEngine rejectFailedWriteWithBatchID:batch.batchID error:error];
+
+ // It's possible that with the completion of this mutation another slot has freed up.
+ [self fillWritePipeline];
+}
+
+- (FSTTransaction *)transaction {
+ return [FSTTransaction transactionWithDatastore:self.datastore];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTSerializerBeta.h b/Firestore/Source/Remote/FSTSerializerBeta.h
new file mode 100644
index 0000000..973f866
--- /dev/null
+++ b/Firestore/Source/Remote/FSTSerializerBeta.h
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 FSTDatabaseID;
+@class FSTDocumentKey;
+@class FSTFieldValue;
+@class FSTMaybeDocument;
+@class FSTMutation;
+@class FSTMutationBatch;
+@class FSTMutationResult;
+@class FSTObjectValue;
+@class FSTQuery;
+@class FSTQueryData;
+@class FSTSnapshotVersion;
+@class FSTTimestamp;
+@class FSTWatchChange;
+
+@class GCFSBatchGetDocumentsResponse;
+@class GCFSDocument;
+@class GCFSDocumentMask;
+@class GCFSListenResponse;
+@class GCFSTarget;
+@class GCFSTarget_DocumentsTarget;
+@class GCFSTarget_QueryTarget;
+@class GCFSValue;
+@class GCFSWrite;
+@class GCFSWriteResult;
+
+@class GPBTimestamp;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Converts internal model objects to their equivalent protocol buffer form. Methods starting with
+ * "encoded" convert to a protocol buffer and methods starting with "decoded" convert from a
+ * protocol buffer.
+ *
+ * Throws an exception if a protocol buffer is missing a critical field or has a value we can't
+ * interpret.
+ */
+@interface FSTSerializerBeta : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID NS_DESIGNATED_INITIALIZER;
+
+- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp;
+- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp;
+
+- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version;
+- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version;
+
+/** Returns the database ID, such as `projects/{project id}/databases/{database_id}`. */
+- (NSString *)encodedDatabaseID;
+
+- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key;
+- (FSTDocumentKey *)decodedDocumentKey:(NSString *)key;
+
+- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue;
+- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto;
+
+- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation;
+- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation;
+
+- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation;
+
+- (nullable NSMutableDictionary<NSString *, NSString *> *)encodedListenRequestLabelsForQueryData:
+ (FSTQueryData *)queryData;
+
+- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData;
+
+- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query;
+- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target;
+
+- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query;
+- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target;
+
+- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange;
+- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange;
+
+- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue key:(FSTDocumentKey *)key;
+
+/**
+ * Encodes an FSTObjectValue into a dictionary.
+ * @return a new dictionary that can be assigned to a field in another proto.
+ */
+- (NSMutableDictionary<NSString *, GCFSValue *> *)encodedFields:(FSTObjectValue *)value;
+
+- (FSTObjectValue *)decodedFields:(NSDictionary<NSString *, GCFSValue *> *)fields;
+
+- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTSerializerBeta.m b/Firestore/Source/Remote/FSTSerializerBeta.m
new file mode 100644
index 0000000..418dabd
--- /dev/null
+++ b/Firestore/Source/Remote/FSTSerializerBeta.m
@@ -0,0 +1,1084 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTSerializerBeta.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+#import "Common.pbobjc.h"
+#import "Document.pbobjc.h"
+#import "Firestore.pbobjc.h"
+#import "Latlng.pbobjc.h"
+#import "Query.pbobjc.h"
+#import "Status.pbobjc.h"
+#import "Write.pbobjc.h"
+
+#import "FIRFirestoreErrors.h"
+#import "FIRGeoPoint.h"
+#import "FSTAssert.h"
+#import "FSTDatabaseID.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTExistenceFilter.h"
+#import "FSTFieldValue.h"
+#import "FSTMutation.h"
+#import "FSTMutationBatch.h"
+#import "FSTPath.h"
+#import "FSTQuery.h"
+#import "FSTQueryData.h"
+#import "FSTSnapshotVersion.h"
+#import "FSTTimestamp.h"
+#import "FSTWatchChange.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTSerializerBeta ()
+@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID;
+@end
+
+@implementation FSTSerializerBeta
+
+- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID {
+ self = [super init];
+ if (self) {
+ _databaseID = databaseID;
+ }
+ return self;
+}
+
+#pragma mark - FSTSnapshotVersion <=> GPBTimestamp
+
+- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp {
+ GPBTimestamp *result = [GPBTimestamp message];
+ result.seconds = timestamp.seconds;
+ result.nanos = timestamp.nanos;
+ return result;
+}
+
+- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp {
+ return [[FSTTimestamp alloc] initWithSeconds:timestamp.seconds nanos:timestamp.nanos];
+}
+
+- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version {
+ return [self encodedTimestamp:version.timestamp];
+}
+
+- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version {
+ return [FSTSnapshotVersion versionWithTimestamp:[self decodedTimestamp:version]];
+}
+
+#pragma mark - FIRGeoPoint <=> GTPLatLng
+
+- (GTPLatLng *)encodedGeoPoint:(FIRGeoPoint *)geoPoint {
+ GTPLatLng *latLng = [GTPLatLng message];
+ latLng.latitude = geoPoint.latitude;
+ latLng.longitude = geoPoint.longitude;
+ return latLng;
+}
+
+- (FIRGeoPoint *)decodedGeoPoint:(GTPLatLng *)latLng {
+ return [[FIRGeoPoint alloc] initWithLatitude:latLng.latitude longitude:latLng.longitude];
+}
+
+#pragma mark - FSTDocumentKey <=> Key proto
+
+- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key {
+ return [self encodedResourcePathForDatabaseID:self.databaseID path:key.path];
+}
+
+- (FSTDocumentKey *)decodedDocumentKey:(NSString *)name {
+ FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:name];
+ FSTAssert([[path segmentAtIndex:1] isEqualToString:self.databaseID.projectID],
+ @"Tried to deserialize key from different project.");
+ FSTAssert([[path segmentAtIndex:3] isEqualToString:self.databaseID.databaseID],
+ @"Tried to deserialize key from different datbase.");
+ return [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]];
+}
+
+- (NSString *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID
+ path:(FSTResourcePath *)path {
+ return [[[[self encodedResourcePathForDatabaseID:databaseID] pathByAppendingSegment:@"documents"]
+ pathByAppendingPath:path] canonicalString];
+}
+
+- (FSTResourcePath *)decodedResourcePathWithDatabaseID:(NSString *)name {
+ FSTResourcePath *path = [FSTResourcePath pathWithString:name];
+ FSTAssert([self validQualifiedResourcePath:path], @"Tried to deserialize invalid key %@", path);
+ return path;
+}
+
+- (NSString *)encodedQueryPath:(FSTResourcePath *)path {
+ if (path.length == 0) {
+ // If the path is empty, the backend requires we leave off the /documents at the end.
+ return [self encodedDatabaseID];
+ }
+ return [self encodedResourcePathForDatabaseID:self.databaseID path:path];
+}
+
+- (FSTResourcePath *)decodedQueryPath:(NSString *)name {
+ FSTResourcePath *resource = [self decodedResourcePathWithDatabaseID:name];
+ if (resource.length == 4) {
+ return [FSTResourcePath pathWithSegments:@[]];
+ } else {
+ return [self localResourcePathForQualifiedResourcePath:resource];
+ }
+}
+
+- (FSTResourcePath *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID {
+ return [FSTResourcePath
+ pathWithSegments:@[ @"projects", databaseID.projectID, @"databases", databaseID.databaseID ]];
+}
+
+- (FSTResourcePath *)localResourcePathForQualifiedResourcePath:(FSTResourcePath *)resourceName {
+ FSTAssert(
+ resourceName.length > 4 && [[resourceName segmentAtIndex:4] isEqualToString:@"documents"],
+ @"Tried to deserialize invalid key %@", resourceName);
+ return [resourceName pathByRemovingFirstSegments:5];
+}
+
+- (BOOL)validQualifiedResourcePath:(FSTResourcePath *)path {
+ return path.length >= 4 && [[path segmentAtIndex:0] isEqualToString:@"projects"] &&
+ [[path segmentAtIndex:2] isEqualToString:@"databases"];
+}
+
+- (NSString *)encodedDatabaseID {
+ return [[self encodedResourcePathForDatabaseID:self.databaseID] canonicalString];
+}
+
+#pragma mark - FSTFieldValue <=> Value proto
+
+- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue {
+ Class class = [fieldValue class];
+ if (class == [FSTNullValue class]) {
+ return [self encodedNull];
+
+ } else if (class == [FSTBooleanValue class]) {
+ return [self encodedBool:[[fieldValue value] boolValue]];
+
+ } else if (class == [FSTIntegerValue class]) {
+ return [self encodedInteger:[[fieldValue value] longLongValue]];
+
+ } else if (class == [FSTDoubleValue class]) {
+ return [self encodedDouble:[[fieldValue value] doubleValue]];
+
+ } else if (class == [FSTStringValue class]) {
+ return [self encodedString:[fieldValue value]];
+
+ } else if (class == [FSTTimestampValue class]) {
+ return [self encodedTimestampValue:((FSTTimestampValue *)fieldValue).internalValue];
+
+ } else if (class == [FSTGeoPointValue class]) {
+ return [self encodedGeoPointValue:[fieldValue value]];
+
+ } else if (class == [FSTBlobValue class]) {
+ return [self encodedBlobValue:[fieldValue value]];
+
+ } else if (class == [FSTReferenceValue class]) {
+ FSTReferenceValue *ref = (FSTReferenceValue *)fieldValue;
+ return [self encodedReferenceValueForDatabaseID:[ref databaseID] key:[ref value]];
+
+ } else if (class == [FSTObjectValue class]) {
+ GCFSValue *result = [GCFSValue message];
+ result.mapValue = [self encodedMapValue:(FSTObjectValue *)fieldValue];
+ return result;
+
+ } else if (class == [FSTArrayValue class]) {
+ GCFSValue *result = [GCFSValue message];
+ result.arrayValue = [self encodedArrayValue:(FSTArrayValue *)fieldValue];
+ return result;
+
+ } else {
+ FSTFail(@"Unhandled type %@ on %@", NSStringFromClass([fieldValue class]), fieldValue);
+ }
+}
+
+- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto {
+ switch (valueProto.valueTypeOneOfCase) {
+ case GCFSValue_ValueType_OneOfCase_NullValue:
+ return [FSTNullValue nullValue];
+
+ case GCFSValue_ValueType_OneOfCase_BooleanValue:
+ return [FSTBooleanValue booleanValue:valueProto.booleanValue];
+
+ case GCFSValue_ValueType_OneOfCase_IntegerValue:
+ return [FSTIntegerValue integerValue:valueProto.integerValue];
+
+ case GCFSValue_ValueType_OneOfCase_DoubleValue:
+ return [FSTDoubleValue doubleValue:valueProto.doubleValue];
+
+ case GCFSValue_ValueType_OneOfCase_StringValue:
+ return [FSTStringValue stringValue:valueProto.stringValue];
+
+ case GCFSValue_ValueType_OneOfCase_TimestampValue:
+ return [FSTTimestampValue timestampValue:[self decodedTimestamp:valueProto.timestampValue]];
+
+ case GCFSValue_ValueType_OneOfCase_GeoPointValue:
+ return [FSTGeoPointValue geoPointValue:[self decodedGeoPoint:valueProto.geoPointValue]];
+
+ case GCFSValue_ValueType_OneOfCase_BytesValue:
+ return [FSTBlobValue blobValue:valueProto.bytesValue];
+
+ case GCFSValue_ValueType_OneOfCase_ReferenceValue:
+ return [self decodedReferenceValue:valueProto.referenceValue];
+
+ case GCFSValue_ValueType_OneOfCase_ArrayValue:
+ return [self decodedArrayValue:valueProto.arrayValue];
+
+ case GCFSValue_ValueType_OneOfCase_MapValue:
+ return [self decodedMapValue:valueProto.mapValue];
+
+ default:
+ FSTFail(@"Unhandled type %d on %@", valueProto.valueTypeOneOfCase, valueProto);
+ }
+}
+
+- (GCFSValue *)encodedNull {
+ GCFSValue *result = [GCFSValue message];
+ result.nullValue = GPBNullValue_NullValue;
+ return result;
+}
+
+- (GCFSValue *)encodedBool:(BOOL)value {
+ GCFSValue *result = [GCFSValue message];
+ result.booleanValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedDouble:(double)value {
+ GCFSValue *result = [GCFSValue message];
+ result.doubleValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedInteger:(int64_t)value {
+ GCFSValue *result = [GCFSValue message];
+ result.integerValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedString:(NSString *)value {
+ GCFSValue *result = [GCFSValue message];
+ result.stringValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedTimestampValue:(FSTTimestamp *)value {
+ GCFSValue *result = [GCFSValue message];
+ result.timestampValue = [self encodedTimestamp:value];
+ return result;
+}
+
+- (GCFSValue *)encodedGeoPointValue:(FIRGeoPoint *)value {
+ GCFSValue *result = [GCFSValue message];
+ result.geoPointValue = [self encodedGeoPoint:value];
+ return result;
+}
+
+- (GCFSValue *)encodedBlobValue:(NSData *)value {
+ GCFSValue *result = [GCFSValue message];
+ result.bytesValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedReferenceValueForDatabaseID:(FSTDatabaseID *)databaseID
+ key:(FSTDocumentKey *)key {
+ GCFSValue *result = [GCFSValue message];
+ result.referenceValue = [self encodedResourcePathForDatabaseID:databaseID path:key.path];
+ return result;
+}
+
+- (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName {
+ FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:resourceName];
+ NSString *project = [path segmentAtIndex:1];
+ NSString *database = [path segmentAtIndex:3];
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:project database:database];
+ FSTDocumentKey *key =
+ [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]];
+ return [FSTReferenceValue referenceValue:key databaseID:databaseID];
+}
+
+- (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue {
+ GCFSArrayValue *proto = [GCFSArrayValue message];
+ NSMutableArray<GCFSValue *> *protoContents = [proto valuesArray];
+
+ [[arrayValue internalValue]
+ enumerateObjectsUsingBlock:^(FSTFieldValue *value, NSUInteger idx, BOOL *stop) {
+ GCFSValue *converted = [self encodedFieldValue:value];
+ [protoContents addObject:converted];
+ }];
+ return proto;
+}
+
+- (FSTArrayValue *)decodedArrayValue:(GCFSArrayValue *)arrayValue {
+ NSMutableArray<FSTFieldValue *> *contents =
+ [NSMutableArray arrayWithCapacity:arrayValue.valuesArray_Count];
+
+ [arrayValue.valuesArray
+ enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger idx, BOOL *stop) {
+ [contents addObject:[self decodedFieldValue:value]];
+ }];
+ return [[FSTArrayValue alloc] initWithValueNoCopy:contents];
+}
+
+- (GCFSMapValue *)encodedMapValue:(FSTObjectValue *)value {
+ GCFSMapValue *result = [GCFSMapValue message];
+ result.fields = [self encodedFields:value];
+ return result;
+}
+
+- (FSTObjectValue *)decodedMapValue:(GCFSMapValue *)map {
+ return [self decodedFields:map.fields];
+}
+
+/**
+ * Encodes an FSTObjectValue into a dictionary.
+ * @return a new dictionary that can be assigned to a field in another proto.
+ */
+- (NSMutableDictionary<NSString *, GCFSValue *> *)encodedFields:(FSTObjectValue *)value {
+ FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *fields = value.internalValue;
+ NSMutableDictionary<NSString *, GCFSValue *> *result = [NSMutableDictionary dictionary];
+ [fields enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) {
+ GCFSValue *converted = [self encodedFieldValue:obj];
+ result[key] = converted;
+ }];
+ return result;
+}
+
+- (FSTObjectValue *)decodedFields:(NSDictionary<NSString *, GCFSValue *> *)fields {
+ __block FSTObjectValue *result = [FSTObjectValue objectValue];
+ [fields enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, GCFSValue *_Nonnull obj,
+ BOOL *_Nonnull stop) {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ key ]];
+ FSTFieldValue *value = [self decodedFieldValue:obj];
+ result = [result objectBySettingValue:value forPath:path];
+ }];
+ return result;
+}
+
+#pragma mark - FSTObjectValue <=> Document proto
+
+- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue
+ key:(FSTDocumentKey *)key {
+ GCFSDocument *proto = [GCFSDocument message];
+ proto.name = [self encodedDocumentKey:key];
+ proto.fields = [self encodedFields:objectValue];
+ return proto;
+}
+
+#pragma mark - FSTMaybeDocument <= BatchGetDocumentsResponse proto
+
+- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response {
+ switch (response.resultOneOfCase) {
+ case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Found:
+ return [self decodedFoundDocument:response];
+ case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Missing:
+ return [self decodedDeletedDocument:response];
+ default:
+ FSTFail(@"Unknown document type: %@", response);
+ }
+}
+
+- (FSTDocument *)decodedFoundDocument:(GCFSBatchGetDocumentsResponse *)response {
+ FSTAssert(!!response.found, @"Tried to deserialize a found document from a deleted document.");
+ FSTDocumentKey *key = [self decodedDocumentKey:response.found.name];
+ FSTObjectValue *value = [self decodedFields:response.found.fields];
+ FSTSnapshotVersion *version = [self decodedVersion:response.found.updateTime];
+ FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]],
+ @"Got a document response with no snapshot version");
+
+ return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO];
+}
+
+- (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response {
+ FSTAssert(!!response.missing, @"Tried to deserialize a deleted document from a found document.");
+ FSTDocumentKey *key = [self decodedDocumentKey:response.missing];
+ FSTSnapshotVersion *version = [self decodedVersion:response.readTime];
+ FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]],
+ @"Got a no document response with no snapshot version");
+ return [FSTDeletedDocument documentWithKey:key version:version];
+}
+
+#pragma mark - FSTMutation => GCFSWrite proto
+
+- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation {
+ GCFSWrite *proto = [GCFSWrite message];
+
+ Class mutationClass = [mutation class];
+ if (mutationClass == [FSTSetMutation class]) {
+ FSTSetMutation *set = (FSTSetMutation *)mutation;
+ proto.update = [self encodedDocumentWithFields:set.value key:set.key];
+
+ } else if (mutationClass == [FSTPatchMutation class]) {
+ FSTPatchMutation *patch = (FSTPatchMutation *)mutation;
+ proto.update = [self encodedDocumentWithFields:patch.value key:patch.key];
+ proto.updateMask = [self encodedFieldMask:patch.fieldMask];
+
+ } else if (mutationClass == [FSTTransformMutation class]) {
+ FSTTransformMutation *transform = (FSTTransformMutation *)mutation;
+
+ proto.transform = [GCFSDocumentTransform message];
+ proto.transform.document = [self encodedDocumentKey:transform.key];
+ proto.transform.fieldTransformsArray = [self encodedFieldTransforms:transform.fieldTransforms];
+ // NOTE: We set a precondition of exists: true as a safety-check, since we always combine
+ // FSTTransformMutations with an FSTSetMutation or FSTPatchMutation which (if successful) should
+ // end up with an existing document.
+ proto.currentDocument.exists = YES;
+
+ } else if (mutationClass == [FSTDeleteMutation class]) {
+ FSTDeleteMutation *delete = (FSTDeleteMutation *)mutation;
+ proto.delete_p = [self encodedDocumentKey:delete.key];
+
+ } else {
+ FSTFail(@"Unknown mutation type %@", NSStringFromClass(mutationClass));
+ }
+
+ if (!mutation.precondition.isNone) {
+ proto.currentDocument = [self encodedPrecondition:mutation.precondition];
+ }
+
+ return proto;
+}
+
+- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation {
+ FSTPrecondition *precondition = [mutation hasCurrentDocument]
+ ? [self decodedPrecondition:mutation.currentDocument]
+ : [FSTPrecondition none];
+
+ switch (mutation.operationOneOfCase) {
+ case GCFSWrite_Operation_OneOfCase_Update:
+ if (mutation.hasUpdateMask) {
+ return [[FSTPatchMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name]
+ fieldMask:[self decodedFieldMask:mutation.updateMask]
+ value:[self decodedFields:mutation.update.fields]
+ precondition:precondition];
+ } else {
+ return [[FSTSetMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name]
+ value:[self decodedFields:mutation.update.fields]
+ precondition:precondition];
+ }
+
+ case GCFSWrite_Operation_OneOfCase_Delete_p:
+ return [[FSTDeleteMutation alloc] initWithKey:[self decodedDocumentKey:mutation.delete_p]
+ precondition:precondition];
+
+ case GCFSWrite_Operation_OneOfCase_Transform: {
+ FSTPreconditionExists exists = precondition.exists;
+ FSTAssert(exists == FSTPreconditionExistsYes,
+ @"Transforms must have precondition \"exists == true\"");
+
+ return [[FSTTransformMutation alloc]
+ initWithKey:[self decodedDocumentKey:mutation.transform.document]
+ fieldTransforms:[self decodedFieldTransforms:mutation.transform.fieldTransformsArray]];
+ }
+
+ default:
+ // Note that insert is intentionally unhandled, since we don't ever deal in them.
+ FSTFail(@"Unknown mutation operation: %d", mutation.operationOneOfCase);
+ }
+}
+
+- (GCFSPrecondition *)encodedPrecondition:(FSTPrecondition *)precondition {
+ FSTAssert(!precondition.isNone, @"Can't serialize an empty precondition");
+ GCFSPrecondition *message = [GCFSPrecondition message];
+ if (precondition.updateTime) {
+ message.updateTime = [self encodedVersion:precondition.updateTime];
+ } else if (precondition.exists != FSTPreconditionExistsNotSet) {
+ message.exists = precondition.exists == FSTPreconditionExistsYes;
+ } else {
+ FSTFail(@"Unknown precondition: %@", precondition);
+ }
+ return message;
+}
+
+- (FSTPrecondition *)decodedPrecondition:(GCFSPrecondition *)precondition {
+ switch (precondition.conditionTypeOneOfCase) {
+ case GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase:
+ return [FSTPrecondition none];
+
+ case GCFSPrecondition_ConditionType_OneOfCase_Exists:
+ return [FSTPrecondition preconditionWithExists:precondition.exists];
+
+ case GCFSPrecondition_ConditionType_OneOfCase_UpdateTime:
+ return [FSTPrecondition
+ preconditionWithUpdateTime:[self decodedVersion:precondition.updateTime]];
+
+ default:
+ FSTFail(@"Unrecognized Precondition one-of case %@", precondition);
+ }
+}
+
+- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask {
+ GCFSDocumentMask *mask = [GCFSDocumentMask message];
+ for (FSTFieldPath *field in fieldMask.fields) {
+ [mask.fieldPathsArray addObject:field.canonicalString];
+ }
+ return mask;
+}
+
+- (FSTFieldMask *)decodedFieldMask:(GCFSDocumentMask *)fieldMask {
+ NSMutableArray<FSTFieldPath *> *fields =
+ [NSMutableArray arrayWithCapacity:fieldMask.fieldPathsArray_Count];
+ for (NSString *path in fieldMask.fieldPathsArray) {
+ [fields addObject:[FSTFieldPath pathWithServerFormat:path]];
+ }
+ return [[FSTFieldMask alloc] initWithFields:fields];
+}
+
+- (NSMutableArray<GCFSDocumentTransform_FieldTransform *> *)encodedFieldTransforms:
+ (NSArray<FSTFieldTransform *> *)fieldTransforms {
+ NSMutableArray *protos = [NSMutableArray array];
+ for (FSTFieldTransform *fieldTransform in fieldTransforms) {
+ FSTAssert([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]],
+ @"Unknown transform: %@", fieldTransform.transform);
+ GCFSDocumentTransform_FieldTransform *proto = [GCFSDocumentTransform_FieldTransform message];
+ proto.fieldPath = fieldTransform.path.canonicalString;
+ proto.setToServerValue = GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime;
+ [protos addObject:proto];
+ }
+ return protos;
+}
+
+- (NSArray<FSTFieldTransform *> *)decodedFieldTransforms:
+ (NSArray<GCFSDocumentTransform_FieldTransform *> *)protos {
+ NSMutableArray<FSTFieldTransform *> *fieldTransforms = [NSMutableArray array];
+ for (GCFSDocumentTransform_FieldTransform *proto in protos) {
+ FSTAssert(
+ proto.setToServerValue == GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime,
+ @"Unknown transform setToServerValue: %d", proto.setToServerValue);
+ [fieldTransforms
+ addObject:[[FSTFieldTransform alloc]
+ initWithPath:[FSTFieldPath pathWithServerFormat:proto.fieldPath]
+ transform:[FSTServerTimestampTransform serverTimestampTransform]]];
+ }
+ return fieldTransforms;
+}
+
+#pragma mark - FSTMutationResult <= GCFSWriteResult proto
+
+- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation {
+ // NOTE: Deletes don't have an updateTime.
+ FSTSnapshotVersion *_Nullable version =
+ mutation.updateTime ? [self decodedVersion:mutation.updateTime] : nil;
+ NSMutableArray *_Nullable transformResults = nil;
+ if (mutation.transformResultsArray.count > 0) {
+ transformResults = [NSMutableArray array];
+ for (GCFSValue *result in mutation.transformResultsArray) {
+ [transformResults addObject:[self decodedFieldValue:result]];
+ }
+ }
+ return [[FSTMutationResult alloc] initWithVersion:version transformResults:transformResults];
+}
+
+#pragma mark - FSTQueryData => GCFSTarget proto
+
+- (nullable NSMutableDictionary<NSString *, NSString *> *)encodedListenRequestLabelsForQueryData:
+ (FSTQueryData *)queryData {
+ NSString *value = [self encodedLabelForPurpose:queryData.purpose];
+ if (!value) {
+ return nil;
+ }
+
+ NSMutableDictionary<NSString *, NSString *> *result =
+ [NSMutableDictionary dictionaryWithCapacity:1];
+ [result setObject:value forKey:@"goog-listen-tags"];
+ return result;
+}
+
+- (nullable NSString *)encodedLabelForPurpose:(FSTQueryPurpose)purpose {
+ switch (purpose) {
+ case FSTQueryPurposeListen:
+ return nil;
+ case FSTQueryPurposeExistenceFilterMismatch:
+ return @"existence-filter-mismatch";
+ case FSTQueryPurposeLimboResolution:
+ return @"limbo-document";
+ default:
+ FSTFail(@"Unrecognized query purpose: %lu", (unsigned long)purpose);
+ }
+}
+
+- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData {
+ GCFSTarget *result = [GCFSTarget message];
+ FSTQuery *query = queryData.query;
+
+ if ([query isDocumentQuery]) {
+ result.documents = [self encodedDocumentsTarget:query];
+ } else {
+ result.query = [self encodedQueryTarget:query];
+ }
+
+ result.targetId = queryData.targetID;
+ if (queryData.resumeToken.length > 0) {
+ result.resumeToken = queryData.resumeToken;
+ }
+
+ return result;
+}
+
+- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query {
+ GCFSTarget_DocumentsTarget *result = [GCFSTarget_DocumentsTarget message];
+ NSMutableArray<NSString *> *docs = result.documentsArray;
+ [docs addObject:[self encodedQueryPath:query.path]];
+ return result;
+}
+
+- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target {
+ NSArray<NSString *> *documents = target.documentsArray;
+ FSTAssert(documents.count == 1, @"DocumentsTarget contained other than 1 document %lu",
+ (unsigned long)documents.count);
+
+ NSString *name = documents[0];
+ return [FSTQuery queryWithPath:[self decodedQueryPath:name]];
+}
+
+- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query {
+ // Dissect the path into parent, collectionId, and optional key filter.
+ GCFSTarget_QueryTarget *queryTarget = [GCFSTarget_QueryTarget message];
+ if (query.path.length == 0) {
+ queryTarget.parent = [self encodedQueryPath:query.path];
+ } else {
+ FSTResourcePath *path = query.path;
+ FSTAssert(path.length % 2 != 0, @"Document queries with filters are not supported.");
+ queryTarget.parent = [self encodedQueryPath:[path pathByRemovingLastSegment]];
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = path.lastSegment;
+ [queryTarget.structuredQuery.fromArray addObject:from];
+ }
+
+ // Encode the filters.
+ GCFSStructuredQuery_Filter *_Nullable where = [self encodedFilters:query.filters];
+ if (where) {
+ queryTarget.structuredQuery.where = where;
+ }
+
+ NSArray<GCFSStructuredQuery_Order *> *orders = [self encodedSortOrders:query.sortOrders];
+ if (orders.count) {
+ [queryTarget.structuredQuery.orderByArray addObjectsFromArray:orders];
+ }
+
+ if (query.limit != NSNotFound) {
+ queryTarget.structuredQuery.limit.value = (int32_t)query.limit;
+ }
+
+ if (query.startAt) {
+ queryTarget.structuredQuery.startAt = [self encodedBound:query.startAt];
+ }
+
+ if (query.endAt) {
+ queryTarget.structuredQuery.endAt = [self encodedBound:query.endAt];
+ }
+
+ return queryTarget;
+}
+
+- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target {
+ FSTResourcePath *path = [self decodedQueryPath:target.parent];
+
+ GCFSStructuredQuery *query = target.structuredQuery;
+ NSUInteger fromCount = query.fromArray_Count;
+ if (fromCount > 0) {
+ FSTAssert(fromCount == 1,
+ @"StructuredQuery.from with more than one collection is not supported.");
+
+ GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0];
+ path = [path pathByAppendingSegment:from.collectionId];
+ }
+
+ NSArray<id<FSTFilter>> *filterBy;
+ if (query.hasWhere) {
+ filterBy = [self decodedFilters:query.where];
+ } else {
+ filterBy = @[];
+ }
+
+ NSArray<FSTSortOrder *> *orderBy;
+ if (query.orderByArray_Count > 0) {
+ orderBy = [self decodedSortOrders:query.orderByArray];
+ } else {
+ orderBy = @[];
+ }
+
+ NSInteger limit = NSNotFound;
+ if (query.hasLimit) {
+ limit = query.limit.value;
+ }
+
+ FSTBound *_Nullable startAt;
+ if (query.hasStartAt) {
+ startAt = [self decodedBound:query.startAt];
+ }
+
+ FSTBound *_Nullable endAt;
+ if (query.hasEndAt) {
+ endAt = [self decodedBound:query.endAt];
+ }
+
+ return [[FSTQuery alloc] initWithPath:path
+ filterBy:filterBy
+ orderBy:orderBy
+ limit:limit
+ startAt:startAt
+ endAt:endAt];
+}
+
+#pragma mark Filters
+
+- (GCFSStructuredQuery_Filter *_Nullable)encodedFilters:(NSArray<id<FSTFilter>> *)filters {
+ if (filters.count == 0) {
+ return nil;
+ }
+ NSMutableArray<GCFSStructuredQuery_Filter *> *protos = [NSMutableArray array];
+ for (id<FSTFilter> filter in filters) {
+ if ([filter isKindOfClass:[FSTRelationFilter class]]) {
+ [protos addObject:[self encodedRelationFilter:filter]];
+ } else {
+ [protos addObject:[self encodedUnaryFilter:filter]];
+ }
+ }
+ if (protos.count == 1) {
+ // Special case: no existing filters and we only need to add one filter. This can be made the
+ // single root filter without a composite filter.
+ return protos[0];
+ }
+ GCFSStructuredQuery_Filter *composite = [GCFSStructuredQuery_Filter message];
+ composite.compositeFilter.op = GCFSStructuredQuery_CompositeFilter_Operator_And;
+ composite.compositeFilter.filtersArray = protos;
+ return composite;
+}
+
+- (NSArray<id<FSTFilter>> *)decodedFilters:(GCFSStructuredQuery_Filter *)proto {
+ NSMutableArray<id<FSTFilter>> *result = [NSMutableArray array];
+
+ NSArray<GCFSStructuredQuery_Filter *> *filters;
+ if (proto.filterTypeOneOfCase ==
+ GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter) {
+ FSTAssert(proto.compositeFilter.op == GCFSStructuredQuery_CompositeFilter_Operator_And,
+ @"Only AND-type composite filters are supported, got %d", proto.compositeFilter.op);
+ filters = proto.compositeFilter.filtersArray;
+ } else {
+ filters = @[ proto ];
+ }
+
+ for (GCFSStructuredQuery_Filter *filter in filters) {
+ switch (filter.filterTypeOneOfCase) {
+ case GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter:
+ FSTFail(@"Nested composite filters are not supported");
+
+ case GCFSStructuredQuery_Filter_FilterType_OneOfCase_FieldFilter:
+ [result addObject:[self decodedRelationFilter:filter.fieldFilter]];
+ break;
+
+ case GCFSStructuredQuery_Filter_FilterType_OneOfCase_UnaryFilter:
+ [result addObject:[self decodedUnaryFilter:filter.unaryFilter]];
+ break;
+
+ default:
+ FSTFail(@"Unrecognized Filter.filterType %d", filter.filterTypeOneOfCase);
+ }
+ }
+ return result;
+}
+
+- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter {
+ GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message];
+ GCFSStructuredQuery_FieldFilter *fieldFilter = proto.fieldFilter;
+ fieldFilter.field = [self encodedFieldPath:filter.field];
+ fieldFilter.op = [self encodedRelationFilterOperator:filter.filterOperator];
+ fieldFilter.value = [self encodedFieldValue:filter.value];
+ return proto;
+}
+
+- (FSTRelationFilter *)decodedRelationFilter:(GCFSStructuredQuery_FieldFilter *)proto {
+ FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath];
+ FSTRelationFilterOperator filterOperator = [self decodedRelationFilterOperator:proto.op];
+ FSTFieldValue *value = [self decodedFieldValue:proto.value];
+ return [FSTRelationFilter filterWithField:fieldPath filterOperator:filterOperator value:value];
+}
+
+- (GCFSStructuredQuery_Filter *)encodedUnaryFilter:(id<FSTFilter>)filter {
+ GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message];
+ proto.unaryFilter.field = [self encodedFieldPath:filter.field];
+ if ([filter isKindOfClass:[FSTNanFilter class]]) {
+ proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNan;
+ } else if ([filter isKindOfClass:[FSTNullFilter class]]) {
+ proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNull;
+ } else {
+ FSTFail(@"Unrecognized filter: %@", filter);
+ }
+ return proto;
+}
+
+- (id<FSTFilter>)decodedUnaryFilter:(GCFSStructuredQuery_UnaryFilter *)proto {
+ FSTFieldPath *field = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath];
+ switch (proto.op) {
+ case GCFSStructuredQuery_UnaryFilter_Operator_IsNan:
+ return [[FSTNanFilter alloc] initWithField:field];
+
+ case GCFSStructuredQuery_UnaryFilter_Operator_IsNull:
+ return [[FSTNullFilter alloc] initWithField:field];
+
+ default:
+ FSTFail(@"Unrecognized UnaryFilter.operator %d", proto.op);
+ }
+}
+
+- (GCFSStructuredQuery_FieldReference *)encodedFieldPath:(FSTFieldPath *)fieldPath {
+ GCFSStructuredQuery_FieldReference *ref = [GCFSStructuredQuery_FieldReference message];
+ ref.fieldPath = fieldPath.canonicalString;
+ return ref;
+}
+
+- (GCFSStructuredQuery_FieldFilter_Operator)encodedRelationFilterOperator:
+ (FSTRelationFilterOperator)filterOperator {
+ switch (filterOperator) {
+ case FSTRelationFilterOperatorLessThan:
+ return GCFSStructuredQuery_FieldFilter_Operator_LessThan;
+ case FSTRelationFilterOperatorLessThanOrEqual:
+ return GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual;
+ case FSTRelationFilterOperatorEqual:
+ return GCFSStructuredQuery_FieldFilter_Operator_Equal;
+ case FSTRelationFilterOperatorGreaterThanOrEqual:
+ return GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual;
+ case FSTRelationFilterOperatorGreaterThan:
+ return GCFSStructuredQuery_FieldFilter_Operator_GreaterThan;
+ default:
+ FSTFail(@"Unhandled FSTRelationFilterOperator: %ld", (long)filterOperator);
+ }
+}
+
+- (FSTRelationFilterOperator)decodedRelationFilterOperator:
+ (GCFSStructuredQuery_FieldFilter_Operator)filterOperator {
+ switch (filterOperator) {
+ case GCFSStructuredQuery_FieldFilter_Operator_LessThan:
+ return FSTRelationFilterOperatorLessThan;
+ case GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual:
+ return FSTRelationFilterOperatorLessThanOrEqual;
+ case GCFSStructuredQuery_FieldFilter_Operator_Equal:
+ return FSTRelationFilterOperatorEqual;
+ case GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual:
+ return FSTRelationFilterOperatorGreaterThanOrEqual;
+ case GCFSStructuredQuery_FieldFilter_Operator_GreaterThan:
+ return FSTRelationFilterOperatorGreaterThan;
+ default:
+ FSTFail(@"Unhandled FieldFilter.operator: %d", filterOperator);
+ }
+}
+
+#pragma mark Property Orders
+
+- (NSArray<GCFSStructuredQuery_Order *> *)encodedSortOrders:(NSArray<FSTSortOrder *> *)orders {
+ NSMutableArray<GCFSStructuredQuery_Order *> *protos = [NSMutableArray array];
+ for (FSTSortOrder *order in orders) {
+ [protos addObject:[self encodedSortOrder:order]];
+ }
+ return protos;
+}
+
+- (NSArray<FSTSortOrder *> *)decodedSortOrders:(NSArray<GCFSStructuredQuery_Order *> *)protos {
+ NSMutableArray<FSTSortOrder *> *result = [NSMutableArray arrayWithCapacity:protos.count];
+ for (GCFSStructuredQuery_Order *orderProto in protos) {
+ [result addObject:[self decodedSortOrder:orderProto]];
+ }
+ return result;
+}
+
+- (GCFSStructuredQuery_Order *)encodedSortOrder:(FSTSortOrder *)sortOrder {
+ GCFSStructuredQuery_Order *proto = [GCFSStructuredQuery_Order message];
+ proto.field = [self encodedFieldPath:sortOrder.field];
+ if (sortOrder.ascending) {
+ proto.direction = GCFSStructuredQuery_Direction_Ascending;
+ } else {
+ proto.direction = GCFSStructuredQuery_Direction_Descending;
+ }
+ return proto;
+}
+
+- (FSTSortOrder *)decodedSortOrder:(GCFSStructuredQuery_Order *)proto {
+ FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath];
+ BOOL ascending;
+ switch (proto.direction) {
+ case GCFSStructuredQuery_Direction_Ascending:
+ ascending = YES;
+ break;
+ case GCFSStructuredQuery_Direction_Descending:
+ ascending = NO;
+ break;
+ default:
+ FSTFail(@"Unrecognized GCFSStructuredQuery_Direction %d", proto.direction);
+ }
+ return [FSTSortOrder sortOrderWithFieldPath:fieldPath ascending:ascending];
+}
+
+#pragma mark - Bounds/Cursors
+
+- (GCFSCursor *)encodedBound:(FSTBound *)bound {
+ GCFSCursor *proto = [GCFSCursor message];
+ proto.before = bound.isBefore;
+ for (FSTFieldValue *fieldValue in bound.position) {
+ GCFSValue *value = [self encodedFieldValue:fieldValue];
+ [proto.valuesArray addObject:value];
+ }
+ return proto;
+}
+
+- (FSTBound *)decodedBound:(GCFSCursor *)proto {
+ NSMutableArray<FSTFieldValue *> *indexComponents = [NSMutableArray array];
+
+ for (GCFSValue *valueProto in proto.valuesArray) {
+ FSTFieldValue *value = [self decodedFieldValue:valueProto];
+ [indexComponents addObject:value];
+ }
+
+ return [FSTBound boundWithPosition:indexComponents isBefore:proto.before];
+}
+
+#pragma mark - FSTWatchChange <= GCFSListenResponse proto
+
+- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange {
+ switch (watchChange.responseTypeOneOfCase) {
+ case GCFSListenResponse_ResponseType_OneOfCase_TargetChange:
+ return [self decodedTargetChangeFromWatchChange:watchChange.targetChange];
+
+ case GCFSListenResponse_ResponseType_OneOfCase_DocumentChange:
+ return [self decodedDocumentChange:watchChange.documentChange];
+
+ case GCFSListenResponse_ResponseType_OneOfCase_DocumentDelete:
+ return [self decodedDocumentDelete:watchChange.documentDelete];
+
+ case GCFSListenResponse_ResponseType_OneOfCase_DocumentRemove:
+ return [self decodedDocumentRemove:watchChange.documentRemove];
+
+ case GCFSListenResponse_ResponseType_OneOfCase_Filter:
+ return [self decodedExistenceFilterWatchChange:watchChange.filter];
+
+ default:
+ FSTFail(@"Unknown WatchChange.changeType %" PRId32, watchChange.responseTypeOneOfCase);
+ }
+}
+
+- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange {
+ // We have only reached a consistent snapshot for the entire stream if there is a read_time set
+ // and it applies to all targets (i.e. the list of targets is empty). The backend is guaranteed to
+ // send such responses.
+ if (watchChange.responseTypeOneOfCase != GCFSListenResponse_ResponseType_OneOfCase_TargetChange) {
+ return [FSTSnapshotVersion noVersion];
+ }
+ if (watchChange.targetChange.targetIdsArray.count != 0) {
+ return [FSTSnapshotVersion noVersion];
+ }
+ return [self decodedVersion:watchChange.targetChange.readTime];
+}
+
+- (FSTWatchTargetChange *)decodedTargetChangeFromWatchChange:(GCFSTargetChange *)change {
+ FSTWatchTargetChangeState state = [self decodedWatchTargetChangeState:change.targetChangeType];
+ NSMutableArray<NSNumber *> *targetIDs =
+ [NSMutableArray arrayWithCapacity:change.targetIdsArray_Count];
+
+ [change.targetIdsArray enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) {
+ [targetIDs addObject:@(value)];
+ }];
+
+ NSError *cause = nil;
+ if (change.hasCause) {
+ cause = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:change.cause.code
+ userInfo:@{NSLocalizedDescriptionKey : change.cause.message}];
+ }
+
+ return [[FSTWatchTargetChange alloc] initWithState:state
+ targetIDs:targetIDs
+ resumeToken:change.resumeToken
+ cause:cause];
+}
+
+- (FSTWatchTargetChangeState)decodedWatchTargetChangeState:
+ (GCFSTargetChange_TargetChangeType)state {
+ switch (state) {
+ case GCFSTargetChange_TargetChangeType_NoChange:
+ return FSTWatchTargetChangeStateNoChange;
+ case GCFSTargetChange_TargetChangeType_Add:
+ return FSTWatchTargetChangeStateAdded;
+ case GCFSTargetChange_TargetChangeType_Remove:
+ return FSTWatchTargetChangeStateRemoved;
+ case GCFSTargetChange_TargetChangeType_Current:
+ return FSTWatchTargetChangeStateCurrent;
+ case GCFSTargetChange_TargetChangeType_Reset:
+ return FSTWatchTargetChangeStateReset;
+ default:
+ FSTFail(@"Unexpected TargetChange.state: %" PRId32, state);
+ }
+}
+
+- (NSArray<NSNumber *> *)decodedIntegerArray:(GPBInt32Array *)values {
+ NSMutableArray<NSNumber *> *result = [NSMutableArray arrayWithCapacity:values.count];
+ [values enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) {
+ [result addObject:@(value)];
+ }];
+ return result;
+}
+
+- (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change {
+ FSTObjectValue *value = [self decodedFields:change.document.fields];
+ FSTDocumentKey *key = [self decodedDocumentKey:change.document.name];
+ FSTSnapshotVersion *version = [self decodedVersion:change.document.updateTime];
+ FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]],
+ @"Got a document change with no snapshot version");
+ FSTMaybeDocument *document =
+ [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO];
+
+ NSArray<NSNumber *> *updatedTargetIds = [self decodedIntegerArray:change.targetIdsArray];
+ NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
+
+ return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedTargetIds
+ removedTargetIDs:removedTargetIds
+ documentKey:document.key
+ document:document];
+}
+
+- (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change {
+ FSTDocumentKey *key = [self decodedDocumentKey:change.document];
+ // Note that version might be unset in which case we use [FSTSnapshotVersion noVersion]
+ FSTSnapshotVersion *version = [self decodedVersion:change.readTime];
+ FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version];
+
+ NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
+
+ return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:removedTargetIds
+ documentKey:document.key
+ document:document];
+}
+
+- (FSTDocumentWatchChange *)decodedDocumentRemove:(GCFSDocumentRemove *)change {
+ FSTDocumentKey *key = [self decodedDocumentKey:change.document];
+ NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
+
+ return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:removedTargetIds
+ documentKey:key
+ document:nil];
+}
+
+- (FSTExistenceFilterWatchChange *)decodedExistenceFilterWatchChange:(GCFSExistenceFilter *)filter {
+ // TODO(dimond): implement existence filter parsing
+ FSTExistenceFilter *existenceFilter = [FSTExistenceFilter filterWithCount:filter.count];
+ FSTTargetID targetID = filter.targetId;
+ return [FSTExistenceFilterWatchChange changeWithFilter:existenceFilter targetID:targetID];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTWatchChange.h b/Firestore/Source/Remote/FSTWatchChange.h
new file mode 100644
index 0000000..6b65279
--- /dev/null
+++ b/Firestore/Source/Remote/FSTWatchChange.h
@@ -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 <Foundation/Foundation.h>
+
+#import "FSTTypes.h"
+
+@class FSTDocumentKey;
+@class FSTExistenceFilter;
+@class FSTMaybeDocument;
+@class FSTSnapshotVersion;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTWatchChange is the internal representation of the watcher API protocol buffers.
+ * This is an empty abstract class so that all the different kinds of changes can have a common
+ * base class.
+ */
+@interface FSTWatchChange : NSObject
+@end
+
+/**
+ * FSTDocumentWatchChange represents a changed document and a list of target ids to which this
+ * change applies. If the document has been deleted, the deleted document will be provided.
+ */
+@interface FSTDocumentWatchChange : FSTWatchChange
+
+- (instancetype)initWithUpdatedTargetIDs:(NSArray<NSNumber *> *)updatedTargetIDs
+ removedTargetIDs:(NSArray<NSNumber *> *)removedTargetIDs
+ documentKey:(FSTDocumentKey *)documentKey
+ document:(nullable FSTMaybeDocument *)document
+ NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** The new document applies to all of these targets. */
+@property(nonatomic, strong, readonly) NSArray<NSNumber *> *updatedTargetIDs;
+
+/** The new document is removed from all of these targets. */
+@property(nonatomic, strong, readonly) NSArray<NSNumber *> *removedTargetIDs;
+
+/** The key of the document for this change. */
+@property(nonatomic, strong, readonly) FSTDocumentKey *documentKey;
+
+/**
+ * The new document or DeletedDocument if it was deleted. Is null if the document went out of
+ * view without the server sending a new document.
+ */
+@property(nonatomic, strong, readonly, nullable) FSTMaybeDocument *document;
+
+@end
+
+/**
+ * An ExistenceFilterWatchChange applies to the targets and is required to verify the current client
+ * state against expected state sent from the server.
+ */
+@interface FSTExistenceFilterWatchChange : FSTWatchChange
+
++ (instancetype)changeWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+@property(nonatomic, strong, readonly) FSTExistenceFilter *filter;
+@property(nonatomic, assign, readonly) FSTTargetID targetID;
+@end
+
+/** FSTWatchTargetChangeState is the kind of change that happened to the watch target. */
+typedef NS_ENUM(NSInteger, FSTWatchTargetChangeState) {
+ FSTWatchTargetChangeStateNoChange,
+ FSTWatchTargetChangeStateAdded,
+ FSTWatchTargetChangeStateRemoved,
+ FSTWatchTargetChangeStateCurrent,
+ FSTWatchTargetChangeStateReset,
+};
+
+/** FSTWatchTargetChange is a change to a watch target. */
+@interface FSTWatchTargetChange : FSTWatchChange
+
+- (instancetype)initWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ resumeToken:(NSData *)resumeToken
+ cause:(nullable NSError *)cause NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** What kind of change occurred to the watch target. */
+@property(nonatomic, assign, readonly) FSTWatchTargetChangeState state;
+
+/** The target IDs that were added/removed/set. */
+@property(nonatomic, strong, readonly) NSArray<NSNumber *> *targetIDs;
+
+/**
+ * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting
+ * without retransmitting all the data that matches the query. The resume token essentially
+ * identifies a point in time from which the server should resume sending results.
+ */
+@property(nonatomic, strong, readonly) NSData *resumeToken;
+
+/** An RPC error indicating why the watch failed. */
+@property(nonatomic, strong, readonly, nullable) NSError *cause;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Remote/FSTWatchChange.m b/Firestore/Source/Remote/FSTWatchChange.m
new file mode 100644
index 0000000..1ace26e
--- /dev/null
+++ b/Firestore/Source/Remote/FSTWatchChange.m
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTWatchChange.h"
+
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTExistenceFilter.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTWatchChange
+@end
+
+@implementation FSTDocumentWatchChange
+
+- (instancetype)initWithUpdatedTargetIDs:(NSArray<NSNumber *> *)updatedTargetIDs
+ removedTargetIDs:(NSArray<NSNumber *> *)removedTargetIDs
+ documentKey:(FSTDocumentKey *)documentKey
+ document:(nullable FSTMaybeDocument *)document {
+ self = [super init];
+ if (self) {
+ _updatedTargetIDs = updatedTargetIDs;
+ _removedTargetIDs = removedTargetIDs;
+ _documentKey = documentKey;
+ _document = document;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isMemberOfClass:[FSTDocumentWatchChange class]]) {
+ return NO;
+ }
+
+ FSTDocumentWatchChange *otherChange = (FSTDocumentWatchChange *)other;
+ return [_updatedTargetIDs isEqual:otherChange.updatedTargetIDs] &&
+ [_removedTargetIDs isEqual:otherChange.removedTargetIDs] &&
+ [_documentKey isEqual:otherChange.documentKey] &&
+ (_document == otherChange.document || [_document isEqual:otherChange.document]);
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = self.updatedTargetIDs.hash;
+ hash = hash * 31 + self.removedTargetIDs.hash;
+ hash = hash * 31 + self.documentKey.hash;
+ hash = hash * 31 + self.document.hash;
+ return hash;
+}
+
+@end
+
+@interface FSTExistenceFilterWatchChange ()
+
+- (instancetype)initWithFilter:(FSTExistenceFilter *)filter
+ targetID:(FSTTargetID)targetID NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation FSTExistenceFilterWatchChange
+
++ (instancetype)changeWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID {
+ return [[FSTExistenceFilterWatchChange alloc] initWithFilter:filter targetID:targetID];
+}
+
+- (instancetype)initWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID {
+ self = [super init];
+ if (self) {
+ _filter = filter;
+ _targetID = targetID;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isMemberOfClass:[FSTExistenceFilterWatchChange class]]) {
+ return NO;
+ }
+
+ FSTExistenceFilterWatchChange *otherChange = (FSTExistenceFilterWatchChange *)other;
+ return [_filter isEqual:otherChange->_filter] && _targetID == otherChange->_targetID;
+}
+
+- (NSUInteger)hash {
+ return self.filter.hash;
+}
+
+@end
+
+@implementation FSTWatchTargetChange
+
+- (instancetype)initWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ resumeToken:(NSData *)resumeToken
+ cause:(nullable NSError *)cause {
+ self = [super init];
+ if (self) {
+ _state = state;
+ _targetIDs = targetIDs;
+ _resumeToken = resumeToken;
+ _cause = cause;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isMemberOfClass:[FSTWatchTargetChange class]]) {
+ return NO;
+ }
+
+ FSTWatchTargetChange *otherChange = (FSTWatchTargetChange *)other;
+ return _state == otherChange->_state && [_targetIDs isEqual:otherChange->_targetIDs] &&
+ [_resumeToken isEqual:otherChange->_resumeToken] &&
+ (_cause == otherChange->_cause || [_cause isEqual:otherChange->_cause]);
+}
+
+- (NSUInteger)hash {
+ NSUInteger hash = (NSUInteger)self.state;
+
+ hash = hash * 31 + self.targetIDs.hash;
+ hash = hash * 31 + self.resumeToken.hash;
+ hash = hash * 31 + self.cause.hash;
+ return hash;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTAssert.h b/Firestore/Source/Util/FSTAssert.h
new file mode 100644
index 0000000..77bbb1d
--- /dev/null
+++ b/Firestore/Source/Util/FSTAssert.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Fails the current Objective-C method if the given condition is false.
+//
+// Unlike NSAssert, this macro is never compiled out if assertions are disabled.
+#define FSTAssert(condition, format, ...) \
+ do { \
+ if (!(condition)) { \
+ FSTFail((format), ##__VA_ARGS__); \
+ } \
+ } while (0)
+
+// Fails the current C function if the given condition is false.
+//
+// Unlike NSCAssert, this macro is never compiled out if assertions are disabled.
+#define FSTCAssert(condition, format, ...) \
+ do { \
+ if (!(condition)) { \
+ FSTCFail((format), ##__VA_ARGS__); \
+ } \
+ } while (0)
+
+// Unconditionally fails the current Objective-C method.
+//
+// This macro fails by calling [[NSAssertionHandler currentHandler] handleFailureInMethod]. It
+// also calls abort(3) in order to make this macro appear to never return, even though the call
+// to handleFailureInMethod itself never returns.
+#define FSTFail(format, ...) \
+ do { \
+ NSString *_file = [NSString stringWithUTF8String:__FILE__]; \
+ NSString *_description = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
+ [[NSAssertionHandler currentHandler] \
+ handleFailureInMethod:_cmd \
+ object:self \
+ file:_file \
+ lineNumber:__LINE__ \
+ description:@"FIRESTORE INTERNAL ASSERTION FAILED: %@", _description]; \
+ abort(); \
+ } while (0)
+
+// Unconditionally fails the current C function.
+//
+// This macro fails by calling [[NSAssertionHandler currentHandler] handleFailureInFunction]. It
+// also calls abort(3) in order to make this macro appear to never return, even though the call
+// to handleFailureInFunction itself never returns.
+#define FSTCFail(format, ...) \
+ do { \
+ NSString *_file = [NSString stringWithUTF8String:__FILE__]; \
+ NSString *_function = [NSString stringWithUTF8String:__PRETTY_FUNCTION__]; \
+ NSString *_description = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
+ [[NSAssertionHandler currentHandler] \
+ handleFailureInFunction:_function \
+ file:_file \
+ lineNumber:__LINE__ \
+ description:@"FIRESTORE INTERNAL ASSERTION FAILED: %@", _description]; \
+ abort(); \
+ } while (0)
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.h b/Firestore/Source/Util/FSTAsyncQueryListener.h
new file mode 100644
index 0000000..0ff1551
--- /dev/null
+++ b/Firestore/Source/Util/FSTAsyncQueryListener.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 "FSTViewSnapshot.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FSTDispatchQueue;
+@class FSTQueryListener;
+
+/**
+ * A wrapper class around FSTQueryListener that dispatches events asynchronously.
+ */
+@interface FSTAsyncQueryListener : NSObject
+
+- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue
+ snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler
+ NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Synchronously mutes the listener and raise no further events. This method is thread safe can be
+ * called from any queue.
+ */
+- (void)mute;
+
+/** Creates an asynchronous version of the provided snapshot handler. */
+- (FSTViewSnapshotHandler)asyncSnapshotHandler;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.m b/Firestore/Source/Util/FSTAsyncQueryListener.m
new file mode 100644
index 0000000..31951e1
--- /dev/null
+++ b/Firestore/Source/Util/FSTAsyncQueryListener.m
@@ -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 "FSTAsyncQueryListener.h"
+
+#import "FSTDispatchQueue.h"
+
+@implementation FSTAsyncQueryListener {
+ volatile BOOL _muted;
+ FSTViewSnapshotHandler _snapshotHandler;
+ FSTDispatchQueue *_dispatchQueue;
+}
+
+- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue
+ snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler {
+ if (self = [super init]) {
+ _dispatchQueue = dispatchQueue;
+ _snapshotHandler = snapshotHandler;
+ }
+ return self;
+}
+
+- (FSTViewSnapshotHandler)asyncSnapshotHandler {
+ return ^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ [_dispatchQueue dispatchAsync:^{
+ if (!_muted) {
+ _snapshotHandler(snapshot, error);
+ }
+ }];
+ };
+}
+
+- (void)mute {
+ _muted = true;
+}
+
+@end
diff --git a/Firestore/Source/Util/FSTClasses.h b/Firestore/Source/Util/FSTClasses.h
new file mode 100644
index 0000000..77dca12
--- /dev/null
+++ b/Firestore/Source/Util/FSTClasses.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// A convenience macro for unimplemented methods. Use as follows:
+//
+// @throw FSTAbstractMethodException(); // NOLINT
+#define FSTAbstractMethodException() \
+ [NSException exceptionWithName:NSInternalInconsistencyException \
+ reason:[NSString stringWithFormat:@"You must override %s in a subclass", \
+ __func__] \
+ userInfo:nil];
+
+// Declare a weak pointer to the given variable
+#define FSTWeakify(var) __weak typeof(var) fstWeakPointerTo##var = var;
+
+// Declare a strong pointer to a variable that's been FSTWeakified. This creates a shadow of the
+// original.
+#define FSTStrongify(var) \
+ _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wshadow\"") \
+ __strong typeof(var) var = fstWeakPointerTo##var; \
+ _Pragma("clang diagnostic pop")
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTComparison.h b/Firestore/Source/Util/FSTComparison.h
new file mode 100644
index 0000000..e6e57e6
--- /dev/null
+++ b/Firestore/Source/Util/FSTComparison.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Compares two NSStrings. */
+NSComparisonResult FSTCompareStrings(NSString *left, NSString *right);
+
+/** Compares two BOOLs. */
+NSComparisonResult FSTCompareBools(BOOL left, BOOL right);
+
+/** Compares two integers. */
+NSComparisonResult FSTCompareInts(int left, int right);
+
+/** Compares two int32_t. */
+NSComparisonResult FSTCompareInt32s(int32_t left, int32_t right);
+
+/** Compares two int64_t. */
+NSComparisonResult FSTCompareInt64s(int64_t left, int64_t right);
+
+/** Compares two NSUIntegers. */
+NSComparisonResult FSTCompareUIntegers(NSUInteger left, NSUInteger right);
+
+/** Compares two doubles (using Firestore semantics for NaN). */
+NSComparisonResult FSTCompareDoubles(double left, double right);
+
+/** Compares a double and an int64_t. */
+NSComparisonResult FSTCompareMixed(double doubleValue, int64_t longValue);
+
+/** Compare two NSData byte sequences. */
+NSComparisonResult FSTCompareBytes(NSData *left, NSData *right);
+
+/** A simple NSComparator for comparing NSNumber instances. */
+extern const NSComparator FSTNumberComparator;
+
+/** A simple NSComparator for comparing NSString instances. */
+extern const NSComparator FSTStringComparator;
+
+/**
+ * Compares the bitwise representation of two doubles, but normalizes NaN values. This is
+ * similar to what the backend and android clients do, including comparing -0.0 as not equal to 0.0.
+ */
+BOOL FSTDoubleBitwiseEquals(double left, double right);
+
+/**
+ * Computes a bitwise hash of a double, but normalizes NaN values, suitable for use when using
+ * FSTDoublesAreBitwiseEqual for equality.
+ */
+NSUInteger FSTDoubleBitwiseHash(double d);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTComparison.m b/Firestore/Source/Util/FSTComparison.m
new file mode 100644
index 0000000..e4f4ccb
--- /dev/null
+++ b/Firestore/Source/Util/FSTComparison.m
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTComparison.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+union DoubleBits {
+ double d;
+ uint64_t bits;
+};
+
+const NSComparator FSTNumberComparator = ^NSComparisonResult(NSNumber *left, NSNumber *right) {
+ return [left compare:right];
+};
+
+const NSComparator FSTStringComparator = ^NSComparisonResult(NSString *left, NSString *right) {
+ return FSTCompareStrings(left, right);
+};
+
+NSComparisonResult FSTCompareStrings(NSString *left, NSString *right) {
+ // NOTE: NSLiteralSearch is necessary to compare the raw character codes. By default,
+ // precomposed characters are considered equivalent to their decomposed equivalents.
+ return [left compare:right options:NSLiteralSearch];
+}
+
+NSComparisonResult FSTCompareBools(BOOL left, BOOL right) {
+ if (!left) {
+ return right ? NSOrderedAscending : NSOrderedSame;
+ } else {
+ return right ? NSOrderedSame : NSOrderedDescending;
+ }
+}
+
+NSComparisonResult FSTCompareInts(int left, int right) {
+ if (left > right) {
+ return NSOrderedDescending;
+ }
+ if (right > left) {
+ return NSOrderedAscending;
+ }
+ return NSOrderedSame;
+}
+
+NSComparisonResult FSTCompareInt32s(int32_t left, int32_t right) {
+ if (left > right) {
+ return NSOrderedDescending;
+ }
+ if (right > left) {
+ return NSOrderedAscending;
+ }
+ return NSOrderedSame;
+}
+
+NSComparisonResult FSTCompareInt64s(int64_t left, int64_t right) {
+ if (left > right) {
+ return NSOrderedDescending;
+ }
+ if (right > left) {
+ return NSOrderedAscending;
+ }
+ return NSOrderedSame;
+}
+
+NSComparisonResult FSTCompareUIntegers(NSUInteger left, NSUInteger right) {
+ if (left > right) {
+ return NSOrderedDescending;
+ }
+ if (right > left) {
+ return NSOrderedAscending;
+ }
+ return NSOrderedSame;
+}
+
+NSComparisonResult FSTCompareDoubles(double left, double right) {
+ // NaN sorts equal to itself and before any other number.
+ if (left < right) {
+ return NSOrderedAscending;
+ } else if (left > right) {
+ return NSOrderedDescending;
+ } else if (left == right) {
+ return NSOrderedSame;
+ } else {
+ // One or both left and right is NaN.
+ if (isnan(left)) {
+ return isnan(right) ? NSOrderedSame : NSOrderedAscending;
+ } else {
+ return NSOrderedDescending;
+ }
+ }
+}
+
+static const double LONG_MIN_VALUE_AS_DOUBLE = (double)LLONG_MIN;
+static const double LONG_MAX_VALUE_AS_DOUBLE = (double)LLONG_MAX;
+
+NSComparisonResult FSTCompareMixed(double doubleValue, int64_t longValue) {
+ // LLONG_MIN has an exact representation as double, so to check for a value outside the range
+ // representable by long, we have to check for strictly less than LLONG_MIN. Note that this also
+ // handles negative infinity.
+ if (doubleValue < LONG_MIN_VALUE_AS_DOUBLE) {
+ return NSOrderedAscending;
+ }
+
+ // LLONG_MAX has no exact representation as double (casting as we've done makes 2^63, which is
+ // larger than LLONG_MAX), so consider any value greater than or equal to the threshold to be out
+ // of range. This also handles positive infinity.
+ if (doubleValue >= LONG_MAX_VALUE_AS_DOUBLE) {
+ return NSOrderedDescending;
+ }
+
+ // In Firestore NaN is defined to compare before all other numbers.
+ if (isnan(doubleValue)) {
+ return NSOrderedAscending;
+ }
+
+ int64_t doubleAsLong = (int64_t)doubleValue;
+ NSComparisonResult cmp = FSTCompareInt64s(doubleAsLong, longValue);
+ if (cmp != NSOrderedSame) {
+ return cmp;
+ }
+
+ // At this point the long representations are equal but this could be due to rounding.
+ double longAsDouble = (double)longValue;
+ return FSTCompareDoubles(doubleValue, longAsDouble);
+}
+
+NSComparisonResult FSTCompareBytes(NSData *left, NSData *right) {
+ NSUInteger minLength = MIN(left.length, right.length);
+ int result = memcmp(left.bytes, right.bytes, minLength);
+ if (result < 0) {
+ return NSOrderedAscending;
+ } else if (result > 0) {
+ return NSOrderedDescending;
+ } else if (left.length < right.length) {
+ return NSOrderedAscending;
+ } else if (left.length > right.length) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+}
+
+/** Helper to normalize a double and then return the raw bits as a uint64_t. */
+uint64_t FSTDoubleBits(double d) {
+ if (isnan(d)) {
+ d = NAN;
+ }
+ union DoubleBits converter = {.d = d};
+ return converter.bits;
+}
+
+BOOL FSTDoubleBitwiseEquals(double left, double right) {
+ return FSTDoubleBits(left) == FSTDoubleBits(right);
+}
+
+NSUInteger FSTDoubleBitwiseHash(double d) {
+ uint64_t bits = FSTDoubleBits(d);
+ // Note that x ^ (x >> 32) works fine for both 32 and 64 bit definitions of NSUInteger
+ return (((NSUInteger)bits) ^ (NSUInteger)(bits >> 32));
+}
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTDispatchQueue.h b/Firestore/Source/Util/FSTDispatchQueue.h
new file mode 100644
index 0000000..da6b3fe
--- /dev/null
+++ b/Firestore/Source/Util/FSTDispatchQueue.h
@@ -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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDispatchQueue : NSObject
+
+/** Creates and returns an FSTDispatchQueue wrapping the specified dispatch_queue_t. */
++ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue;
+
+- (instancetype)init __attribute__((unavailable("Use static constructor method.")));
+
+/**
+ * Asserts that we are already running on this queue (actually, we can only verify that the
+ * queue's label is the same, but hopefully that's good enough.)
+ */
+- (void)verifyIsCurrentQueue;
+
+/**
+ * Same as dispatch_async() except it asserts that we're not already on the queue, since this
+ * generally indicates a bug (and can lead to re-ordering of operations, etc).
+ *
+ * @param block The block to run.
+ */
+- (void)dispatchAsync:(void (^)())block;
+
+/**
+ * Unlike dispatchAsync: this method does not require you to dispatch to a different queue than
+ * the current one (thus it is equivalent to a raw dispatch_async()).
+ *
+ * This is useful, e.g. for dispatching to the user's queue directly from user API call (in which
+ * case we don't know if we're already on the user's queue or not).
+ *
+ * @param block The block to run.
+ */
+- (void)dispatchAsyncAllowingSameQueue:(void (^)())block;
+
+/** The underlying wrapped dispatch_queue_t */
+@property(nonatomic, strong, readonly) dispatch_queue_t queue;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTDispatchQueue.m b/Firestore/Source/Util/FSTDispatchQueue.m
new file mode 100644
index 0000000..8d55d28
--- /dev/null
+++ b/Firestore/Source/Util/FSTDispatchQueue.m
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 "FSTAssert.h"
+#import "FSTDispatchQueue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDispatchQueue ()
+- (instancetype)initWithQueue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER;
+@end
+
+@implementation FSTDispatchQueue
+
++ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue {
+ return [[FSTDispatchQueue alloc] initWithQueue:dispatchQueue];
+}
+
+- (instancetype)initWithQueue:(dispatch_queue_t)queue {
+ if (self = [super init]) {
+ _queue = queue;
+ }
+ return self;
+}
+
+- (void)verifyIsCurrentQueue {
+ FSTAssert([self onTargetQueue],
+ @"We are running on the wrong dispatch queue. Expected '%@' Actual: '%@'",
+ [self targetQueueLabel], [self currentQueueLabel]);
+}
+
+- (void)dispatchAsync:(void (^)())block {
+ FSTAssert(![self onTargetQueue],
+ @"dispatchAsync called when we are already running on target dispatch queue '%@'",
+ [self targetQueueLabel]);
+
+ dispatch_async(self.queue, block);
+}
+
+- (void)dispatchAsyncAllowingSameQueue:(void (^)())block {
+ dispatch_async(self.queue, block);
+}
+
+#pragma mark - Private Methods
+
+- (NSString *)currentQueueLabel {
+ return [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)];
+}
+
+- (NSString *)targetQueueLabel {
+ return [NSString stringWithUTF8String:dispatch_queue_get_label(self.queue)];
+}
+
+- (BOOL)onTargetQueue {
+ return [[self currentQueueLabel] isEqualToString:[self targetQueueLabel]];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTLogger.h b/Firestore/Source/Util/FSTLogger.h
new file mode 100644
index 0000000..699570a
--- /dev/null
+++ b/Firestore/Source/Util/FSTLogger.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Logs to NSLog if [FIRFirestore isLoggingEnabled] is YES. */
+void FSTLog(NSString *format, ...) NS_FORMAT_FUNCTION(1, 2);
+
+void FSTWarn(NSString *format, ...) NS_FORMAT_FUNCTION(1, 2);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTLogger.m b/Firestore/Source/Util/FSTLogger.m
new file mode 100644
index 0000000..396c788
--- /dev/null
+++ b/Firestore/Source/Util/FSTLogger.m
@@ -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 "FSTLogger.h"
+
+#import "FIRFirestore+Internal.h"
+#import "FIRLogger.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+void FSTLog(NSString *format, ...) {
+ if ([FIRFirestore isLoggingEnabled]) {
+ va_list args;
+ va_start(args, format);
+ FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerFirestore, @"I-FST000001", format, args);
+ va_end(args);
+ }
+}
+
+void FSTWarn(NSString *format, ...) {
+ va_list args;
+ va_start(args, format);
+ FIRLogBasic(FIRLoggerLevelWarning, kFIRLoggerFirestore, @"I-FST000001", format, args);
+ va_end(args);
+}
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTUsageValidation.h b/Firestore/Source/Util/FSTUsageValidation.h
new file mode 100644
index 0000000..a80dafa
--- /dev/null
+++ b/Firestore/Source/Util/FSTUsageValidation.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.
+ */
+
+#include <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Helper for creating a general exception for invalid usage of an API. */
+NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...);
+
+/**
+ * Macro to throw exceptions in response to API usage errors. Avoids the lint warning you usually
+ * get when using @throw and (unlike a function) doesn't trigger warnings about not all codepaths
+ * returning a value.
+ *
+ * Exceptions should only be used for programmer errors made by consumers of the SDK, e.g.
+ * invalid method arguments.
+ *
+ * For recoverable runtime errors, use NSError**.
+ * For internal programming errors, use FSTFail().
+ */
+#define FSTThrowInvalidUsage(exceptionName, format, ...) \
+ do { \
+ @throw FSTInvalidUsage(exceptionName, format, ##__VA_ARGS__); \
+ } while (0)
+
+#define FSTThrowInvalidArgument(format, ...) \
+ do { \
+ @throw FSTInvalidUsage(@"FIRInvalidArgumentException", format, ##__VA_ARGS__); \
+ } while (0)
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTUsageValidation.m b/Firestore/Source/Util/FSTUsageValidation.m
new file mode 100644
index 0000000..82128f4
--- /dev/null
+++ b/Firestore/Source/Util/FSTUsageValidation.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.
+ */
+
+#include <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...) {
+ va_list arg_list;
+ va_start(arg_list, format);
+ NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list];
+ va_end(arg_list);
+
+ return [[NSException alloc] initWithName:exceptionName reason:formattedString userInfo:nil];
+}
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTUtil.h b/Firestore/Source/Util/FSTUtil.h
new file mode 100644
index 0000000..3985d10
--- /dev/null
+++ b/Firestore/Source/Util/FSTUtil.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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTUtil : NSObject
+
+/** Generates a random double between 0 and 1. */
++ (double)randomDouble;
+
+/** Generates a random ID suitable for use as a document ID. */
++ (NSString *)autoID;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/Util/FSTUtil.m b/Firestore/Source/Util/FSTUtil.m
new file mode 100644
index 0000000..d14c429
--- /dev/null
+++ b/Firestore/Source/Util/FSTUtil.m
@@ -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 "FSTUtil.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static const double kArc4RandomMax = 0x100000000;
+
+static const int kAutoIDLength = 20;
+static NSString *const kAutoIDAlphabet =
+ @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+@implementation FSTUtil
+
++ (double)randomDouble {
+ return ((double)arc4random() / kArc4RandomMax);
+}
+
++ (NSString *)autoID {
+ unichar autoID[kAutoIDLength];
+ for (int i = 0; i < kAutoIDLength; i++) {
+ uint32_t randIndex = arc4random_uniform((uint32_t)kAutoIDAlphabet.length);
+ autoID[i] = [kAutoIDAlphabet characterAtIndex:randIndex];
+ }
+ return [NSString stringWithCharacters:autoID length:kAutoIDLength];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/test.sh b/Firestore/test.sh
new file mode 100755
index 0000000..7e26e3f
--- /dev/null
+++ b/Firestore/test.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+# Copyright 2017 Google
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -euo pipefail
+
+FIRESTORE_DIR=$(dirname "${BASH_SOURCE[0]}")
+
+test_iOS() {
+ xcodebuild \
+ -workspace "$FIRESTORE_DIR/Example/Firestore.xcworkspace" \
+ -scheme Firestore_Tests \
+ -sdk iphonesimulator \
+ -destination 'platform=iOS Simulator,name=iPhone 7' \
+ build \
+ test \
+ ONLY_ACTIVE_ARCH=YES \
+ CODE_SIGNING_REQUIRED=NO \
+ | xcpretty
+
+ xcodebuild \
+ -workspace "$FIRESTORE_DIR/Example/Firestore.xcworkspace" \
+ -scheme SwiftBuildTest \
+ -sdk iphonesimulator \
+ -destination 'platform=iOS Simulator,name=iPhone 7' \
+ build \
+ ONLY_ACTIVE_ARCH=YES \
+ CODE_SIGNING_REQUIRED=NO \
+ | xcpretty
+}
+
+test_iOS; RESULT=$?
+if [[ $RESULT == 65 ]]; then
+ echo "xcodebuild exited with 65, retrying"
+ sleep 5
+
+ test_iOS; RESULT=$?
+fi
+
+exit $RESULT
diff --git a/Firestore/third_party/Immutable/FSTArraySortedDictionary.h b/Firestore/third_party/Immutable/FSTArraySortedDictionary.h
new file mode 100644
index 0000000..4b78360
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTArraySortedDictionary.h
@@ -0,0 +1,35 @@
+#import <Foundation/Foundation.h>
+
+#import "FSTImmutableSortedDictionary.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTArraySortedDictionary is an array backed implementation of FSTImmutableSortedDictionary.
+ *
+ * You should not use this class directly. You should use FSTImmutableSortedDictionary.
+ *
+ * FSTArraySortedDictionary uses arrays and linear lookups to achieve good memory efficiency while
+ * maintaining good performance for small collections. It also uses fewer allocations than a
+ * comparable red black tree. To avoid degrading performance with increasing collection size it
+ * will automatically convert to a FSTTreeSortedDictionary after an insert call above a certain
+ * threshold.
+ */
+@interface FSTArraySortedDictionary <KeyType, ValueType> :
+ FSTImmutableSortedDictionary<KeyType, ValueType>
+
++ (FSTArraySortedDictionary<KeyType, ValueType> *)
+ dictionaryWithDictionary:(NSDictionary<KeyType, ValueType> *)dictionary
+ comparator:(NSComparator)comparator;
+
+- (id)init __attribute__((unavailable("Use initWithComparator:keys:values: instead.")));
+
+- (instancetype)initWithComparator:(NSComparator)comparator;
+
+- (instancetype)initWithComparator:(NSComparator)comparator
+ keys:(NSArray<KeyType> *)keys
+ values:(NSArray<ValueType> *)values NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTArraySortedDictionary.m b/Firestore/third_party/Immutable/FSTArraySortedDictionary.m
new file mode 100644
index 0000000..fd3bfd7
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTArraySortedDictionary.m
@@ -0,0 +1,242 @@
+#import "FSTArraySortedDictionary.h"
+
+#import "FSTArraySortedDictionaryEnumerator.h"
+#import "FSTAssert.h"
+#import "FSTTreeSortedDictionary.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTArraySortedDictionary ()
+@property(nonatomic, copy, readwrite) NSComparator comparator;
+@property(nonatomic, strong) NSArray<id> *keys;
+@property(nonatomic, strong) NSArray<id> *values;
+@end
+
+@implementation FSTArraySortedDictionary
+
++ (FSTArraySortedDictionary *)dictionaryWithDictionary:(NSDictionary *)dictionary
+ comparator:(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 FSTImmutableSortedDictionary with keys "
+ @"with same ordering!"];
+ }
+ }
+ }];
+
+ NSMutableArray *values = [NSMutableArray arrayWithCapacity:keys.count];
+ NSInteger pos = 0;
+ for (id key in keys) {
+ values[pos++] = dictionary[key];
+ }
+ FSTAssert(values.count == keys.count, @"We added as many keys as values");
+ return [[FSTArraySortedDictionary alloc] initWithComparator:comparator keys:keys values:values];
+}
+
+- (id)initWithComparator:(NSComparator)comparator {
+ return [self initWithComparator:comparator keys:[NSArray array] values:[NSArray array]];
+}
+
+// Designated initializer.
+- (id)initWithComparator:(NSComparator)comparator keys:(NSArray *)keys values:(NSArray *)values {
+ self = [super init];
+ if (self != nil) {
+ FSTAssert(keys.count == values.count, @"keys and values must have the same count");
+ _comparator = comparator;
+ _keys = keys;
+ _values = values;
+ }
+ return self;
+}
+
+/** Returns the index of the first position where array[position] >= key. */
+- (int)findInsertPositionForKey:(id)key {
+ int 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;
+}
+
+- (FSTImmutableSortedDictionary *)dictionaryBySettingObject:(id)value forKey:(id)key {
+ 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 >= kSortedDictionaryArrayToRBTreeSizeThreshold) {
+ 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 [FSTTreeSortedDictionary dictionaryWithDictionary:dict comparator: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 [[FSTArraySortedDictionary 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 [[FSTArraySortedDictionary alloc] initWithComparator:self.comparator
+ keys:newKeys
+ values:newValues];
+ }
+}
+
+- (FSTImmutableSortedDictionary *)dictionaryByRemovingObjectForKey:(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 [[FSTArraySortedDictionary alloc] initWithComparator:self.comparator
+ keys:newKeys
+ values:newValues];
+ }
+}
+
+- (nullable id)objectForKey:(id)key {
+ NSInteger pos = [self findKey:key];
+ if (pos == NSNotFound) {
+ return nil;
+ } else {
+ return self.values[pos];
+ }
+}
+
+- (nullable id)predecessorKey:(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];
+ }
+}
+
+- (NSUInteger)indexOfKey:(id)key {
+ return [self findKey:key];
+}
+
+- (BOOL)isEmpty {
+ return self.keys.count == 0;
+}
+
+- (NSUInteger)count {
+ return 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)containsKey:(id)key {
+ return [self findKey:key] != NSNotFound;
+}
+
+- (NSEnumerator *)keyEnumerator {
+ return [self.keys objectEnumerator];
+}
+
+- (NSEnumerator *)keyEnumeratorFrom:(id)startKey {
+ return [self keyEnumeratorFrom:startKey to:nil];
+}
+
+- (NSEnumerator *)keyEnumeratorFrom:(id)startKey to:(nullable id)endKey {
+ int start = [self findInsertPositionForKey:startKey];
+ int end = (int)self.count;
+ if (endKey) {
+ end = [self findInsertPositionForKey:endKey];
+ }
+ return [[FSTArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys
+ startPos:start
+ endPos:end
+ isReverse:NO];
+}
+
+- (NSEnumerator *)reverseKeyEnumerator {
+ return [self.keys reverseObjectEnumerator];
+}
+
+- (NSEnumerator *)reverseKeyEnumeratorFrom:(id)startKey {
+ int 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 [[FSTArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys
+ startPos:startPos
+ endPos:-1
+ isReverse:YES];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.h b/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.h
new file mode 100644
index 0000000..36c4f32
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.h
@@ -0,0 +1,26 @@
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTArraySortedDictionaryEnumerator <KeyType, ValueType> : NSEnumerator<ValueType>
+
+- (id)init __attribute__((unavailable("Use initWithKeys:startPos:endPos:isReverse: instead.")));
+
+/**
+ * An enumerator for use with a dictionary.
+ *
+ * @param keys The keys to enumerator within.
+ * @param start The index of the initial key to return.
+ * @param end If end is after (or equal to) start (or before, if reverse), then the enumerator will
+ * stop and not return the value once it reaches end.
+ */
+- (instancetype)initWithKeys:(NSArray<KeyType> *)keys
+ startPos:(int)start
+ endPos:(int)end
+ isReverse:(BOOL)reverse NS_DESIGNATED_INITIALIZER;
+
+- (_Nullable ValueType)nextObject;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.m b/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.m
new file mode 100644
index 0000000..e8fdfcc
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.m
@@ -0,0 +1,54 @@
+#import "FSTArraySortedDictionaryEnumerator.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// clang-format off
+// For some reason, clang-format messes this line up...
+@interface FSTArraySortedDictionaryEnumerator<KeyType, ValueType> ()
+@property(nonatomic, assign) int pos;
+@property(nonatomic, assign) int start;
+@property(nonatomic, assign) int end;
+@property(nonatomic, assign) BOOL reverse;
+@property(nonatomic, strong) NSArray<KeyType> *keys;
+@end
+// clang-format on
+
+@implementation FSTArraySortedDictionaryEnumerator
+
+- (id)initWithKeys:(NSArray *)keys startPos:(int)start endPos:(int)end isReverse:(BOOL)reverse {
+ self = [super init];
+ if (self != nil) {
+ _keys = keys;
+ _start = start;
+ _end = end;
+ _pos = start;
+ _reverse = reverse;
+ }
+ return self;
+}
+
+- (nullable id)nextObject {
+ if (self.pos < 0 || self.pos >= self.keys.count) {
+ return nil;
+ }
+ if (self.reverse) {
+ if (self.pos <= self.end) {
+ return nil;
+ }
+ } else {
+ if (self.pos >= self.end) {
+ return nil;
+ }
+ }
+ int pos = self.pos;
+ if (self.reverse) {
+ self.pos--;
+ } else {
+ self.pos++;
+ }
+ return self.keys[pos];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h b/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h
new file mode 100644
index 0000000..a2264ec
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h
@@ -0,0 +1,120 @@
+/**
+ * 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>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * 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.
+ */
+extern const int kSortedDictionaryArrayToRBTreeSizeThreshold;
+
+/**
+ * FSTImmutableSortedDictionary is a dictionary. It is immutable, but has methods to create new
+ * dictionaries that are mutations of it, in an efficient way.
+ */
+@interface FSTImmutableSortedDictionary <KeyType, __covariant ValueType> : NSObject
+
++ (FSTImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator;
++ (FSTImmutableSortedDictionary *)dictionaryWithDictionary:
+ (NSDictionary<KeyType, ValueType> *)dictionary
+ comparator:(NSComparator)comparator;
+
+/**
+ * Creates a new dictionary identical to this one, but with a key-value pair added or updated.
+ *
+ * @param aValue The value to associate with the key.
+ * @param aKey The key to insert/update.
+ * @return A new dictionary with the added/updated value.
+ */
+- (FSTImmutableSortedDictionary<KeyType, ValueType> *)dictionaryBySettingObject:(ValueType)aValue
+ forKey:(KeyType)aKey;
+
+/**
+ * Creates a new dictionary identical to this one, but with a key removed from it.
+ *
+ * @param aKey The key to remove.
+ * @return A new dictionary without that value.
+ */
+- (FSTImmutableSortedDictionary<KeyType, ValueType> *)dictionaryByRemovingObjectForKey:
+ (KeyType)aKey;
+
+/**
+ * Looks up a value in the dictionary.
+ *
+ * @param key The key to look up.
+ * @return The value for the key, if present.
+ */
+- (nullable ValueType)objectForKey:(KeyType)key;
+
+/**
+ * Looks up a value in the dictionary.
+ *
+ * @param key The key to look up.
+ * @return The value for the key, if present.
+ */
+- (ValueType)objectForKeyedSubscript:(KeyType)key;
+
+/**
+ * Gets the key before the given key in sorted order.
+ *
+ * @param key The key to look before.
+ * @return The key before the given one.
+ */
+- (nullable KeyType)predecessorKey:(KeyType)key;
+
+/**
+ * Returns the index of the key or NSNotFound if the key is not found.
+ *
+ * @param key The key to return the index for.
+ * @return The index of the key, or NSNotFound if key not found.
+ */
+- (NSUInteger)indexOfKey:(KeyType)key;
+
+/** Returns true if the dictionary contains no elements. */
+- (BOOL)isEmpty;
+
+/** Returns the number of items in this dictionary. */
+- (NSUInteger)count;
+
+/** Returns the smallest key in this dictionary. */
+- (KeyType)minKey;
+
+/** Returns the largest key in this dictionary. */
+- (KeyType)maxKey;
+
+/** Calls the given block with each of the items in this dictionary, in order. */
+- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(KeyType key, ValueType value, BOOL *stop))block;
+
+/** Calls the given block with each of the items in this dictionary, in reverse order. */
+- (void)enumerateKeysAndObjectsReverse:(BOOL)reverse
+ usingBlock:(void (^)(KeyType key, ValueType value, BOOL *stop))block;
+
+/** Returns true if the dictionary contains the given key. */
+- (BOOL)containsKey:(KeyType)key;
+
+- (NSEnumerator<KeyType> *)keyEnumerator;
+- (NSEnumerator<KeyType> *)keyEnumeratorFrom:(KeyType)startKey;
+/** Enumerator for the range [startKey, endKey). */
+- (NSEnumerator<KeyType> *)keyEnumeratorFrom:(KeyType)startKey to:(nullable KeyType)endKey;
+- (NSEnumerator<KeyType> *)reverseKeyEnumerator;
+- (NSEnumerator<KeyType> *)reverseKeyEnumeratorFrom:(KeyType)startKey;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.m b/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.m
new file mode 100644
index 0000000..a858529
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.m
@@ -0,0 +1,143 @@
+#import "FSTImmutableSortedDictionary.h"
+
+#import "FSTArraySortedDictionary.h"
+#import "FSTClasses.h"
+#import "FSTTreeSortedDictionary.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+const int kSortedDictionaryArrayToRBTreeSizeThreshold = 25;
+
+@implementation FSTImmutableSortedDictionary
+
++ (FSTImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator {
+ return [[FSTArraySortedDictionary alloc] initWithComparator:comparator];
+}
+
++ (FSTImmutableSortedDictionary *)dictionaryWithDictionary:(NSDictionary *)dictionary
+ comparator:(NSComparator)comparator {
+ if (dictionary.count <= kSortedDictionaryArrayToRBTreeSizeThreshold) {
+ return [FSTArraySortedDictionary dictionaryWithDictionary:dictionary comparator:comparator];
+ } else {
+ return [FSTTreeSortedDictionary dictionaryWithDictionary:dictionary comparator:comparator];
+ }
+}
+
+- (FSTImmutableSortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (FSTImmutableSortedDictionary *)dictionaryByRemovingObjectForKey:(id)aKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FSTImmutableSortedDictionary class]]) {
+ return NO;
+ }
+
+ // TODO(klimt): We could make this more efficient if we put the comparison inside the
+ // implementations and short-circuit if they share the same tree node, for instance.
+ FSTImmutableSortedDictionary *other = (FSTImmutableSortedDictionary *)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;
+}
+
+- (nullable id)objectForKey:(id)key {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (id)objectForKeyedSubscript:(id)key {
+ return [self objectForKey:key];
+}
+
+- (nullable id)predecessorKey:(id)key {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSUInteger)indexOfKey:(id)key {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (BOOL)isEmpty {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSUInteger)count {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (id)minKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (id)maxKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (BOOL)containsKey:(id)key {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSEnumerator *)keyEnumerator {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSEnumerator *)keyEnumeratorFrom:(id)startKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSEnumerator *)keyEnumeratorFrom:(id)startKey to:(nullable id)endKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSEnumerator *)reverseKeyEnumerator {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (NSEnumerator *)reverseKeyEnumeratorFrom:(id)startKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTImmutableSortedSet.h b/Firestore/third_party/Immutable/FSTImmutableSortedSet.h
new file mode 100644
index 0000000..d0f9906
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTImmutableSortedSet.h
@@ -0,0 +1,47 @@
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTImmutableSortedSet is a set. It is immutable, but has methods to create new sets that are
+ * mutations of it, in an efficient way.
+ */
+@interface FSTImmutableSortedSet <KeyType> : NSObject
+
++ (FSTImmutableSortedSet<KeyType> *)setWithComparator:(NSComparator)comparator;
+
++ (FSTImmutableSortedSet<KeyType> *)setWithKeysFromDictionary:(NSDictionary<KeyType, id> *)array
+ comparator:(NSComparator)comparator;
+
+- (BOOL)containsObject:(KeyType)object;
+
+- (FSTImmutableSortedSet<KeyType> *)setByAddingObject:(KeyType)object;
+- (FSTImmutableSortedSet<KeyType> *)setByRemovingObject:(KeyType)object;
+
+- (KeyType)firstObject;
+- (KeyType)lastObject;
+- (NSUInteger)count;
+- (BOOL)isEmpty;
+
+- (KeyType)predecessorObject:(KeyType)entry;
+
+/**
+ * Returns the index of the object or NSNotFound if the object is not found.
+ *
+ * @param object The object to return the index for.
+ * @return The index of the object, or NSNotFound if not found.
+ */
+- (NSUInteger)indexOfObject:(KeyType)object;
+
+- (void)enumerateObjectsUsingBlock:(void (^)(KeyType obj, BOOL *stop))block;
+- (void)enumerateObjectsFrom:(KeyType)start
+ to:(_Nullable KeyType)end
+ usingBlock:(void (^)(KeyType obj, BOOL *stop))block;
+- (void)enumerateObjectsReverse:(BOOL)reverse usingBlock:(void (^)(KeyType obj, BOOL *stop))block;
+
+- (NSEnumerator<KeyType> *)objectEnumerator;
+- (NSEnumerator<KeyType> *)objectEnumeratorFrom:(KeyType)startKey;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTImmutableSortedSet.m b/Firestore/third_party/Immutable/FSTImmutableSortedSet.m
new file mode 100644
index 0000000..a13a79e
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTImmutableSortedSet.m
@@ -0,0 +1,144 @@
+#import "FSTImmutableSortedSet.h"
+
+#import "FSTImmutableSortedDictionary.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTImmutableSortedSet ()
+@property(nonatomic, strong) FSTImmutableSortedDictionary *dictionary;
+@end
+
+@implementation FSTImmutableSortedSet
+
++ (FSTImmutableSortedSet *)setWithComparator:(NSComparator)comparator {
+ return [FSTImmutableSortedSet setWithKeysFromDictionary:@{} comparator:comparator];
+}
+
++ (FSTImmutableSortedSet *)setWithKeysFromDictionary:(NSDictionary *)dictionary
+ comparator:(NSComparator)comparator {
+ FSTImmutableSortedDictionary *setDict =
+ [FSTImmutableSortedDictionary dictionaryWithDictionary:dictionary comparator:comparator];
+ return [[FSTImmutableSortedSet alloc] initWithDictionary:setDict];
+}
+
+// Designated initializer.
+- (id)initWithDictionary:(FSTImmutableSortedDictionary *)dictionary {
+ self = [super init];
+ if (self != nil) {
+ _dictionary = dictionary;
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)object {
+ if (![object isKindOfClass:[FSTImmutableSortedSet class]]) {
+ return NO;
+ }
+
+ FSTImmutableSortedSet *other = (FSTImmutableSortedSet *)object;
+
+ return [self.dictionary isEqual:other.dictionary];
+}
+
+- (NSUInteger)hash {
+ return [self.dictionary hash];
+}
+
+- (BOOL)containsObject:(id)object {
+ return [self.dictionary containsKey:object];
+}
+
+- (FSTImmutableSortedSet *)setByAddingObject:(id)object {
+ FSTImmutableSortedDictionary *newDictionary =
+ [self.dictionary dictionaryBySettingObject:[NSNull null] forKey:object];
+ if (newDictionary != self.dictionary) {
+ return [[FSTImmutableSortedSet alloc] initWithDictionary:newDictionary];
+ } else {
+ return self;
+ }
+}
+
+- (FSTImmutableSortedSet *)setByRemovingObject:(id)object {
+ FSTImmutableSortedDictionary *newDictionary =
+ [self.dictionary dictionaryByRemovingObjectForKey:object];
+ if (newDictionary != self.dictionary) {
+ return [[FSTImmutableSortedSet alloc] initWithDictionary:newDictionary];
+ } else {
+ return self;
+ }
+}
+
+- (id)firstObject {
+ return [self.dictionary minKey];
+}
+
+- (id)lastObject {
+ return [self.dictionary maxKey];
+}
+
+- (id)predecessorObject:(id)entry {
+ return [self.dictionary predecessorKey:entry];
+}
+
+- (NSUInteger)indexOfObject:(id)object {
+ return [self.dictionary indexOfKey:object];
+}
+
+- (NSUInteger)count {
+ return [self.dictionary count];
+}
+
+- (BOOL)isEmpty {
+ return [self.dictionary isEmpty];
+}
+
+- (void)enumerateObjectsUsingBlock:(void (^)(id, BOOL *))block {
+ [self enumerateObjectsReverse:NO usingBlock:block];
+}
+
+- (void)enumerateObjectsFrom:(id)start to:(_Nullable id)end usingBlock:(void (^)(id, BOOL *))block {
+ NSEnumerator *enumerator = [self.dictionary keyEnumeratorFrom:start to:end];
+ id item = [enumerator nextObject];
+ while (item) {
+ BOOL stop = NO;
+ block(item, &stop);
+ if (stop) {
+ return;
+ }
+ item = [enumerator nextObject];
+ }
+}
+
+- (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];
+}
+
+- (NSEnumerator *)objectEnumeratorFrom:(id)startKey {
+ return [self.dictionary keyEnumeratorFrom:startKey];
+}
+
+- (NSString *)description {
+ NSMutableString *str = [[NSMutableString alloc] init];
+ __block BOOL first = YES;
+ [str appendString:@"FSTImmutableSortedSet ( "];
+ [self enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
+ if (!first) {
+ [str appendString:@", "];
+ }
+ first = NO;
+ [str appendString:[NSString stringWithFormat:@"%@", obj]];
+ }];
+ [str appendString:@" )"];
+ return str;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTLLRBEmptyNode.h b/Firestore/third_party/Immutable/FSTLLRBEmptyNode.h
new file mode 100644
index 0000000..4c3c2af
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTLLRBEmptyNode.h
@@ -0,0 +1,11 @@
+#import <Foundation/Foundation.h>
+
+#import "FSTLLRBNode.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLLRBEmptyNode : NSObject <FSTLLRBNode>
++ (instancetype)emptyNode;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTLLRBEmptyNode.m b/Firestore/third_party/Immutable/FSTLLRBEmptyNode.m
new file mode 100644
index 0000000..ec49c2c
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTLLRBEmptyNode.m
@@ -0,0 +1,102 @@
+#import "FSTLLRBEmptyNode.h"
+
+#import "FSTLLRBValueNode.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTLLRBEmptyNode
+
+- (NSString *)description {
+ return @"[empty node]";
+}
+
++ (instancetype)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;
+}
+
+- (nullable id)key {
+ return nil;
+}
+
+- (nullable id)value {
+ return nil;
+}
+
+- (FSTLLRBColor)color {
+ return FSTLLRBColorUnspecified;
+}
+
+- (nullable id<FSTLLRBNode>)left {
+ return nil;
+}
+
+- (nullable id<FSTLLRBNode>)right {
+ return nil;
+}
+
+- (instancetype)copyWith:(id _Nullable)aKey
+ withValue:(id _Nullable)aValue
+ withColor:(FSTLLRBColor)aColor
+ withLeft:(id<FSTLLRBNode> _Nullable)aLeft
+ withRight:(id<FSTLLRBNode> _Nullable)aRight {
+ // This class is a singleton anyway, so this is more efficient than calling the constructor again.
+ return self;
+}
+
+- (id<FSTLLRBNode>)insertKey:(id)aKey forValue:(id)aValue withComparator:(NSComparator)aComparator {
+ FSTLLRBValueNode *result = [[FSTLLRBValueNode alloc] initWithKey:aKey
+ withValue:aValue
+ withColor:FSTLLRBColorUnspecified
+ withLeft:nil
+ withRight:nil];
+ return result;
+}
+
+- (id<FSTLLRBNode>)remove:(id)key withComparator:(NSComparator)aComparator {
+ return self;
+}
+
+- (NSUInteger)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<FSTLLRBNode>)min {
+ return self;
+}
+
+- (nullable id)minKey {
+ return nil;
+}
+
+- (nullable id)maxKey {
+ return nil;
+}
+
+- (BOOL)isRed {
+ return NO;
+}
+
+- (int)check {
+ return 0;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTLLRBNode.h b/Firestore/third_party/Immutable/FSTLLRBNode.h
new file mode 100644
index 0000000..082b875
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTLLRBNode.h
@@ -0,0 +1,68 @@
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A FSTLLRBColor is the color of a tree node. It can be RED, BLACK, or unset.
+ */
+typedef NS_ENUM(NSInteger, FSTLLRBColor) {
+ FSTLLRBColorUnspecified = 0,
+ FSTLLRBColorRed = 1,
+ FSTLLRBColorBlack = 2,
+};
+
+/**
+ * FSTLLRBNode is the interface for a node in a FSTTreeSortedDictionary.
+ */
+@protocol FSTLLRBNode <NSObject>
+
+/**
+ * Creates a copy of the given node, changing any values that were specified.
+ * For any parameter that is left as nil, this instance's value will be used.
+ */
+- (instancetype)copyWith:(nullable id)aKey
+ withValue:(nullable id)aValue
+ withColor:(FSTLLRBColor)aColor
+ withLeft:(nullable id<FSTLLRBNode>)aLeft
+ withRight:(nullable id<FSTLLRBNode>)aRight;
+
+/** Returns a tree node with the given key-value pair set/updated. */
+- (id<FSTLLRBNode>)insertKey:(id)aKey forValue:(id)aValue withComparator:(NSComparator)aComparator;
+
+/** Returns a tree node with the given key removed. */
+- (id<FSTLLRBNode>)remove:(id)key withComparator:(NSComparator)aComparator;
+
+/** Returns the number of elements at this node or beneath it in the tree. */
+- (NSUInteger)count;
+
+/** Returns true if this is an FSTLLRBEmptyNode -- a leaf node in the tree. */
+- (BOOL)isEmpty;
+
+- (BOOL)inorderTraversal:(BOOL (^)(id key, id value))action;
+- (BOOL)reverseTraversal:(BOOL (^)(id key, id value))action;
+
+/** Returns the left-most node under (or including) this node. */
+- (id<FSTLLRBNode>)min;
+
+/** Returns the key of the left-most node under (or including) this node. */
+- (nullable id)minKey;
+
+/** Returns the key of the right-most node under (or including) this node. */
+- (nullable id)maxKey;
+
+/** Returns true if this node is red (as opposed to black). */
+- (BOOL)isRed;
+
+/** Checks that this node and below it hold the red-black invariants. Throws otherwise. */
+- (int)check;
+
+// Accessors for properties.
+- (nullable id)key;
+- (nullable id)value;
+- (FSTLLRBColor)color;
+- (nullable id<FSTLLRBNode>)left;
+- (nullable id<FSTLLRBNode>)right;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTLLRBValueNode.h b/Firestore/third_party/Immutable/FSTLLRBValueNode.h
new file mode 100644
index 0000000..4a0873c
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTLLRBValueNode.h
@@ -0,0 +1,29 @@
+#import <Foundation/Foundation.h>
+
+#import "FSTLLRBNode.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLLRBValueNode : NSObject <FSTLLRBNode>
+
+- (id)init __attribute__((
+ unavailable("Use initWithKey:withValue:withColor:withLeft:withRight: instead.")));
+
+- (instancetype)initWithKey:(nullable id)key
+ withValue:(nullable id)value
+ withColor:(FSTLLRBColor)color
+ withLeft:(nullable id<FSTLLRBNode>)left
+ withRight:(nullable id<FSTLLRBNode>)right NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, assign, readonly) FSTLLRBColor color;
+@property(nonatomic, strong, readonly, nullable) id key;
+@property(nonatomic, strong, readonly, nullable) id value;
+@property(nonatomic, strong, readonly, nullable) id<FSTLLRBNode> right;
+
+// This property cannot be readonly, because it is set when building the tree.
+// TODO(klimt): Find a way to build the tree without mutating this.
+@property(nonatomic, strong, nullable) id<FSTLLRBNode> left;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTLLRBValueNode.m b/Firestore/third_party/Immutable/FSTLLRBValueNode.m
new file mode 100644
index 0000000..d048012
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTLLRBValueNode.m
@@ -0,0 +1,308 @@
+#import "FSTLLRBValueNode.h"
+
+#import "FSTAssert.h"
+#import "FSTLLRBEmptyNode.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLLRBValueNode ()
+@property(nonatomic, assign) FSTLLRBColor color;
+@property(nonatomic, assign) NSUInteger count;
+@property(nonatomic, strong) id key;
+@property(nonatomic, strong) id value;
+@property(nonatomic, strong) id<FSTLLRBNode> right;
+@end
+
+@implementation FSTLLRBValueNode
+
+- (NSString *)colorDescription {
+ NSString *color = @"unspecified";
+ if (self.color == FSTLLRBColorRed) {
+ color = @"red";
+ } else if (self.color == FSTLLRBColorBlack) {
+ color = @"black";
+ }
+ return color;
+}
+
+- (NSString *)description {
+ NSString *color = self.colorDescription;
+ return [NSString stringWithFormat:@"[key=%@ val=%@ color=%@]", self.key, self.value, color];
+}
+
+// Designated initializer.
+- (instancetype)initWithKey:(id _Nullable)aKey
+ withValue:(id _Nullable)aValue
+ withColor:(FSTLLRBColor)aColor
+ withLeft:(id<FSTLLRBNode> _Nullable)aLeft
+ withRight:(id<FSTLLRBNode> _Nullable)aRight {
+ self = [super init];
+ if (self) {
+ _key = aKey;
+ _value = aValue;
+ _color = aColor != FSTLLRBColorUnspecified ? aColor : FSTLLRBColorRed;
+ _left = aLeft != nil ? aLeft : [FSTLLRBEmptyNode emptyNode];
+ _right = aRight != nil ? aRight : [FSTLLRBEmptyNode emptyNode];
+ _count = NSNotFound;
+ }
+ return self;
+}
+
+- (instancetype)copyWith:(id _Nullable)aKey
+ withValue:(id _Nullable)aValue
+ withColor:(FSTLLRBColor)aColor
+ withLeft:(id<FSTLLRBNode> _Nullable)aLeft
+ withRight:(id<FSTLLRBNode> _Nullable)aRight {
+ return [[FSTLLRBValueNode alloc]
+ initWithKey:(aKey != nil) ? aKey : self.key
+ withValue:(aValue != nil) ? aValue : self.value
+ withColor:(aColor != FSTLLRBColorUnspecified) ? aColor : self.color
+ withLeft:(aLeft != nil) ? aLeft : self.left
+ withRight:(aRight != nil) ? aRight : self.right];
+}
+
+- (void)setLeft:(nullable id<FSTLLRBNode>)left {
+ // Setting the left node should be only done by the builder, so doing it after someone has
+ // memoized count is an error.
+ FSTAssert(_count == NSNotFound, @"Can't update left node after using count");
+ _left = left;
+}
+
+- (NSUInteger)count {
+ if (_count == NSNotFound) {
+ _count = _left.count + 1 + _right.count;
+ }
+ return _count;
+}
+
+- (BOOL)isEmpty {
+ return NO;
+}
+
+/**
+ * Early terminates if action 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<FSTLLRBNode>)min {
+ if ([self.left isEmpty]) {
+ return self;
+ } else {
+ return [self.left min];
+ }
+}
+
+- (nullable id)minKey {
+ return [[self min] key];
+}
+
+- (nullable id)maxKey {
+ if ([self.right isEmpty]) {
+ return self.key;
+ } else {
+ return [self.right maxKey];
+ }
+}
+
+- (id<FSTLLRBNode>)insertKey:(id)aKey forValue:(id)aValue withComparator:(NSComparator)aComparator {
+ NSComparisonResult cmp = aComparator(aKey, self.key);
+ FSTLLRBValueNode *n = self;
+
+ if (cmp == NSOrderedAscending) {
+ n = [n copyWith:nil
+ withValue:nil
+ withColor:FSTLLRBColorUnspecified
+ withLeft:[n.left insertKey:aKey forValue:aValue withComparator:aComparator]
+ withRight:nil];
+ } else if (cmp == NSOrderedSame) {
+ n = [n copyWith:nil
+ withValue:aValue
+ withColor:FSTLLRBColorUnspecified
+ withLeft:nil
+ withRight:nil];
+ } else {
+ n = [n copyWith:nil
+ withValue:nil
+ withColor:FSTLLRBColorUnspecified
+ withLeft:nil
+ withRight:[n.right insertKey:aKey forValue:aValue withComparator:aComparator]];
+ }
+
+ return [n fixUp];
+}
+
+- (id<FSTLLRBNode>)removeMin {
+ if ([self.left isEmpty]) {
+ return [FSTLLRBEmptyNode emptyNode];
+ }
+
+ FSTLLRBValueNode *n = self;
+ if (![n.left isRed] && ![n.left.left isRed]) {
+ n = [n moveRedLeft];
+ }
+
+ n = [n copyWith:nil
+ withValue:nil
+ withColor:FSTLLRBColorUnspecified
+ withLeft:[(FSTLLRBValueNode *)n.left removeMin]
+ withRight:nil];
+ return [n fixUp];
+}
+
+- (id<FSTLLRBNode>)fixUp {
+ FSTLLRBValueNode *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;
+}
+
+- (FSTLLRBValueNode *)moveRedLeft {
+ FSTLLRBValueNode *n = [self colorFlip];
+ if ([n.right.left isRed]) {
+ n = [n copyWith:nil
+ withValue:nil
+ withColor:FSTLLRBColorUnspecified
+ withLeft:nil
+ withRight:[(FSTLLRBValueNode *)n.right rotateRight]];
+ n = [n rotateLeft];
+ n = [n colorFlip];
+ }
+ return n;
+}
+
+- (FSTLLRBValueNode *)moveRedRight {
+ FSTLLRBValueNode *n = [self colorFlip];
+ if ([n.left.left isRed]) {
+ n = [n rotateRight];
+ n = [n colorFlip];
+ }
+ return n;
+}
+
+- (id<FSTLLRBNode>)rotateLeft {
+ id<FSTLLRBNode> nl = [self copyWith:nil
+ withValue:nil
+ withColor:FSTLLRBColorRed
+ withLeft:nil
+ withRight:self.right.left];
+ return [self.right copyWith:nil withValue:nil withColor:self.color withLeft:nl withRight:nil];
+}
+
+- (id<FSTLLRBNode>)rotateRight {
+ id<FSTLLRBNode> nr = [self copyWith:nil
+ withValue:nil
+ withColor:FSTLLRBColorRed
+ withLeft:self.left.right
+ withRight:nil];
+ return [self.left copyWith:nil withValue:nil withColor:self.color withLeft:nil withRight:nr];
+}
+
+- (id<FSTLLRBNode>)colorFlip {
+ FSTLLRBColor color = self.color == FSTLLRBColorBlack ? FSTLLRBColorRed : FSTLLRBColorBlack;
+ FSTLLRBColor leftColor =
+ self.left.color == FSTLLRBColorBlack ? FSTLLRBColorRed : FSTLLRBColorBlack;
+ FSTLLRBColor rightColor =
+ self.right.color == FSTLLRBColorBlack ? FSTLLRBColorRed : FSTLLRBColorBlack;
+
+ id<FSTLLRBNode> nleft =
+ [self.left copyWith:nil withValue:nil withColor:leftColor withLeft:nil withRight:nil];
+ id<FSTLLRBNode> nright =
+ [self.right copyWith:nil withValue:nil withColor:rightColor withLeft:nil withRight:nil];
+
+ return [self copyWith:nil withValue:nil withColor:color withLeft:nleft withRight:nright];
+}
+
+- (id<FSTLLRBNode>)remove:(id)aKey withComparator:(NSComparator)comparator {
+ id<FSTLLRBNode> smallest;
+ FSTLLRBValueNode *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:FSTLLRBColorUnspecified
+ 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 [FSTLLRBEmptyNode emptyNode];
+ } else {
+ smallest = [n.right min];
+ n = [n copyWith:smallest.key
+ withValue:smallest.value
+ withColor:FSTLLRBColorUnspecified
+ withLeft:nil
+ withRight:[(FSTLLRBValueNode *)n.right removeMin]];
+ }
+ }
+ n = [n copyWith:nil
+ withValue:nil
+ withColor:FSTLLRBColorUnspecified
+ withLeft:nil
+ withRight:[n.right remove:aKey withComparator:comparator]];
+ }
+ return [n fixUp];
+}
+
+- (BOOL)isRed {
+ return self.color == FSTLLRBColorRed;
+}
+
+- (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];
+ if (blackDepth != [self.right check]) {
+ NSString *err =
+ [NSString stringWithFormat:@"(%@ -> %@)blackDepth: %d ; self.right check: %d", self.value,
+ self.colorDescription, blackDepth, [self.right check]];
+ @throw [[NSException alloc] initWithName:@"check" reason:err userInfo:nil];
+ } else {
+ int ret = blackDepth + ([self isRed] ? 0 : 1);
+ return ret;
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTTreeSortedDictionary.h b/Firestore/third_party/Immutable/FSTTreeSortedDictionary.h
new file mode 100644
index 0000000..6c26e5f
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTTreeSortedDictionary.h
@@ -0,0 +1,41 @@
+/**
+ * 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 "FSTImmutableSortedDictionary.h"
+#import "FSTLLRBNode.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTTreeSortedDictionary is a tree-based implementation of FSTImmutableSortedDictionary.
+ * You should not use this class directly. You should use FSTImmutableSortedDictionary.
+ */
+@interface FSTTreeSortedDictionary <KeyType, ValueType> :
+ FSTImmutableSortedDictionary<KeyType, ValueType>
+
+@property(nonatomic, copy, readonly) NSComparator comparator;
+@property(nonatomic, strong, readonly) id<FSTLLRBNode> root;
+
+- (id)init __attribute__((unavailable("Use initWithComparator:withRoot: instead.")));
+
+- (instancetype)initWithComparator:(NSComparator)aComparator;
+
+- (instancetype)initWithComparator:(NSComparator)aComparator
+ withRoot:(id<FSTLLRBNode>)aRoot NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTTreeSortedDictionary.m b/Firestore/third_party/Immutable/FSTTreeSortedDictionary.m
new file mode 100644
index 0000000..4059951
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTTreeSortedDictionary.m
@@ -0,0 +1,382 @@
+#import "FSTTreeSortedDictionary.h"
+
+#import "FSTLLRBEmptyNode.h"
+#import "FSTLLRBValueNode.h"
+#import "FSTTreeSortedDictionaryEnumerator.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTTreeSortedDictionary ()
+
+- (FSTTreeSortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey;
+
+@property(nonatomic, strong) id<FSTLLRBNode> root;
+@property(nonatomic, copy, readwrite) NSComparator comparator;
+@end
+
+@implementation FSTTreeSortedDictionary
+
++ (FSTTreeSortedDictionary *)dictionaryWithDictionary:(NSDictionary *)dictionary
+ comparator:(NSComparator)comparator {
+ __block FSTTreeSortedDictionary *dict =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:comparator];
+ [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ dict = [dict dictionaryBySettingObject:obj forKey:key];
+ }];
+ return dict;
+}
+
+- (id)initWithComparator:(NSComparator)aComparator {
+ return [self initWithComparator:aComparator withRoot:[FSTLLRBEmptyNode emptyNode]];
+}
+
+// Designated initializer.
+- (id)initWithComparator:(NSComparator)aComparator withRoot:(id<FSTLLRBNode>)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.
+ */
+- (FSTTreeSortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey {
+ return [[FSTTreeSortedDictionary alloc]
+ initWithComparator:self.comparator
+ withRoot:[[self.root insertKey:aKey forValue:aValue withComparator:self.comparator]
+ copyWith:nil
+ withValue:nil
+ withColor:FSTLLRBColorBlack
+ withLeft:nil
+ withRight:nil]];
+}
+
+- (FSTTreeSortedDictionary *)dictionaryByRemovingObjectForKey:(id)aKey {
+ // Remove is somewhat expensive even if the key doesn't exist (the tree does rebalancing and
+ // stuff). So avoid it.
+ if (![self containsKey:aKey]) {
+ return self;
+ } else {
+ return [[FSTTreeSortedDictionary alloc]
+ initWithComparator:self.comparator
+ withRoot:[[self.root remove:aKey withComparator:self.comparator]
+ copyWith:nil
+ withValue:nil
+ withColor:FSTLLRBColorBlack
+ withLeft:nil
+ withRight:nil]];
+ }
+}
+
+- (nullable id)objectForKey:(id)key {
+ NSComparisonResult cmp;
+ id<FSTLLRBNode> 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;
+}
+
+- (nullable id)predecessorKey:(id)key {
+ NSComparisonResult cmp;
+ id<FSTLLRBNode> node = self.root;
+ id<FSTLLRBNode> 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]}];
+}
+
+- (NSUInteger)indexOfKey:(id)key {
+ NSUInteger prunedNodes = 0;
+ id<FSTLLRBNode> node = self.root;
+ while (![node isEmpty]) {
+ NSComparisonResult cmp = self.comparator(key, node.key);
+ if (cmp == NSOrderedSame) {
+ return prunedNodes + node.left.count;
+ } else if (cmp == NSOrderedAscending) {
+ node = node.left;
+ } else if (cmp == NSOrderedDescending) {
+ prunedNodes += node.left.count + 1;
+ node = node.right;
+ }
+ }
+ return NSNotFound;
+}
+
+- (BOOL)isEmpty {
+ return [self.root isEmpty];
+}
+
+- (NSUInteger)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)containsKey:(id)key {
+ return ([self objectForKey:key] != nil);
+}
+
+- (NSEnumerator *)keyEnumerator {
+ return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self
+ startKey:nil
+ endKey:nil
+ isReverse:NO];
+}
+
+- (NSEnumerator *)keyEnumeratorFrom:(id)startKey {
+ return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self
+ startKey:startKey
+ endKey:nil
+ isReverse:NO];
+}
+
+- (NSEnumerator *)keyEnumeratorFrom:(id)startKey to:(nullable id)endKey {
+ return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self
+ startKey:startKey
+ endKey:endKey
+ isReverse:NO];
+}
+
+- (NSEnumerator *)reverseKeyEnumerator {
+ return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self
+ startKey:nil
+ endKey:nil
+ isReverse:YES];
+}
+
+- (NSEnumerator *)reverseKeyEnumeratorFrom:(id)startKey {
+ return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self
+ startKey:startKey
+ endKey:nil
+ isReverse:YES];
+}
+
+#pragma mark -
+#pragma mark Tree Builder
+
+// Code to efficiently build a red black tree.
+
+typedef struct {
+ unsigned int bits;
+ unsigned short count;
+ unsigned short current;
+} Base12List;
+
+unsigned int LogBase2(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 Base12ListNext(Base12List *list) {
+ BOOL result = !(list->bits & (0x1 << list->current));
+ list->current--;
+ return result;
+}
+
+static inline unsigned BitMask(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}
+ */
+Base12List *NewBase12List(unsigned int length) {
+ size_t sz = sizeof(Base12List);
+ Base12List *list = calloc(1, sz);
+ // Calculate the number of bits that we care about
+ list->count = (unsigned short)LogBase2(length + 1);
+ unsigned int mask = BitMask(list->count);
+ list->bits = (length + 1) & mask;
+ list->current = list->count - 1;
+ return list;
+}
+
+void FreeBase12List(Base12List *list) {
+ free(list);
+}
+
++ (id<FSTLLRBNode>)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 [[FSTLLRBValueNode alloc] initWithKey:key
+ withValue:dictionary[key]
+ withColor:FSTLLRBColorBlack
+ withLeft:nil
+ withRight:nil];
+ } else {
+ NSUInteger middle = length / 2;
+ id<FSTLLRBNode> left = [FSTTreeSortedDictionary buildBalancedTree:keys
+ dictionary:dictionary
+ subArrayStartIndex:startIndex
+ length:middle];
+ id<FSTLLRBNode> right = [FSTTreeSortedDictionary buildBalancedTree:keys
+ dictionary:dictionary
+ subArrayStartIndex:(startIndex + middle + 1)
+ length:middle];
+ id key = keys[startIndex + middle];
+ return [[FSTLLRBValueNode alloc] initWithKey:key
+ withValue:dictionary[key]
+ withColor:FSTLLRBColorBlack
+ withLeft:left
+ withRight:right];
+ }
+}
+
++ (nullable id<FSTLLRBNode>)rootFrom12List:(Base12List *)base12List
+ keyList:(NSArray *)keyList
+ dictionary:(NSDictionary *)dictionary {
+ __block FSTLLRBValueNode *root = nil;
+ __block FSTLLRBValueNode *node = nil;
+ __block NSUInteger index = keyList.count;
+
+ void (^buildPennant)(FSTLLRBColor, NSUInteger) = ^(FSTLLRBColor color, NSUInteger chunkSize) {
+ NSUInteger startIndex = index - chunkSize + 1;
+ index -= chunkSize;
+ id key = keyList[index];
+ FSTLLRBValueNode *childTree = [self buildBalancedTree:keyList
+ dictionary:dictionary
+ subArrayStartIndex:startIndex
+ length:(chunkSize - 1)];
+ FSTLLRBValueNode *pennant = [[FSTLLRBValueNode alloc] initWithKey:key
+ withValue:dictionary[key]
+ withColor:color
+ withLeft:nil
+ withRight:childTree];
+ if (node) {
+ // This is the only place this property is set.
+ node.left = pennant;
+ node = pennant;
+ } else {
+ root = pennant;
+ node = pennant;
+ }
+ };
+
+ for (int i = 0; i < base12List->count; ++i) {
+ BOOL isOne = Base12ListNext(base12List);
+ NSUInteger chunkSize = (NSUInteger)pow(2.0, base12List->count - (i + 1));
+ if (isOne) {
+ buildPennant(FSTLLRBColorBlack, chunkSize);
+ } else {
+ buildPennant(FSTLLRBColorBlack, chunkSize);
+ buildPennant(FSTLLRBColorRed, chunkSize);
+ }
+ }
+ return root;
+}
+
+/**
+ * Uses the algorithm linked here:
+ * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.1458
+ */
++ (FSTImmutableSortedDictionary *)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 FSTImmutableSortedDictionary "
+ @"with keys with same ordering!"];
+ }
+ }
+ }];
+
+ Base12List *list = NewBase12List((unsigned int)sortedKeyList.count);
+ id<FSTLLRBNode> root = [self rootFrom12List:list keyList:sortedKeyList dictionary:dictionary];
+ FreeBase12List(list);
+
+ if (root != nil) {
+ return [[FSTTreeSortedDictionary alloc] initWithComparator:comparator withRoot:root];
+ } else {
+ return [[FSTTreeSortedDictionary alloc] initWithComparator:comparator];
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.h b/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.h
new file mode 100644
index 0000000..ab82f00
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.h
@@ -0,0 +1,21 @@
+#import <Foundation/Foundation.h>
+
+#import "FSTTreeSortedDictionary.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTTreeSortedDictionaryEnumerator <KeyType, ValueType> : NSEnumerator<ValueType>
+
+- (id)init __attribute__((
+ unavailable("Use initWithImmutableSortedDictionary:startKey:isReverse: instead.")));
+
+- (instancetype)initWithImmutableSortedDictionary:
+ (FSTTreeSortedDictionary<KeyType, ValueType> *)aDict
+ startKey:(KeyType _Nullable)startKey
+ endKey:(KeyType _Nullable)endKey
+ isReverse:(BOOL)reverse NS_DESIGNATED_INITIALIZER;
+- (nullable ValueType)nextObject;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.m b/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.m
new file mode 100644
index 0000000..bfdfba5
--- /dev/null
+++ b/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.m
@@ -0,0 +1,114 @@
+#import "FSTTreeSortedDictionaryEnumerator.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// clang-format off
+// For some reason, clang-format messes this line up...
+@interface FSTTreeSortedDictionaryEnumerator<KeyType, ValueType> ()
+/** The dictionary being enumerated. */
+@property(nonatomic, strong) FSTTreeSortedDictionary<KeyType, ValueType> *immutableSortedDictionary;
+/** The stack of tree nodes above the current node that will need to be revisited later. */
+@property(nonatomic, strong) NSMutableArray<id<FSTLLRBNode>> *stack;
+/** The direction of the traversal. YES=Descending. NO=Ascending. */
+@property(nonatomic, assign) BOOL isReverse;
+/** If set, the enumerator should stop at this key and not return it. */
+@property(nonatomic, strong, nullable) id endKey;
+@end
+// clang-format on
+
+@implementation FSTTreeSortedDictionaryEnumerator
+
+- (instancetype)initWithImmutableSortedDictionary:(FSTTreeSortedDictionary *)aDict
+ startKey:(id _Nullable)startKey
+ endKey:(id _Nullable)endKey
+ isReverse:(BOOL)reverse {
+ self = [super init];
+ if (self) {
+ _immutableSortedDictionary = aDict;
+ _stack = [[NSMutableArray alloc] init];
+ _isReverse = reverse;
+ _endKey = endKey;
+
+ NSComparator comparator = aDict.comparator;
+ id<FSTLLRBNode> node = aDict.root;
+
+ NSComparisonResult comparedToStart;
+ NSComparisonResult comparedToEnd;
+ while (![node isEmpty]) {
+ comparedToStart = NSOrderedDescending;
+ if (startKey) {
+ comparedToStart = comparator(node.key, startKey);
+ if (reverse) {
+ comparedToStart *= -1;
+ }
+ }
+ comparedToEnd = NSOrderedAscending;
+ if (endKey) {
+ comparedToEnd = comparator(node.key, endKey);
+ if (reverse) {
+ comparedToEnd *= -1;
+ }
+ }
+
+ if (comparedToStart == NSOrderedAscending) {
+ // This node is less than our start key. Ignore it.
+ if (reverse) {
+ node = node.left;
+ } else {
+ node = node.right;
+ }
+ } else if (comparedToStart == NSOrderedSame) {
+ // This node is exactly equal to our start key. If it's less than the end key, push it on
+ // the stack, but stop iterating.
+ if (comparedToEnd == NSOrderedAscending) {
+ [_stack addObject:node];
+ }
+ break;
+ } else {
+ // This node is greater than our start key. If it's less than our end key, add it to the
+ // stack and move on to the next one.
+ if (comparedToEnd == NSOrderedAscending) {
+ [_stack addObject:node];
+ }
+ if (reverse) {
+ node = node.right;
+ } else {
+ node = node.left;
+ }
+ }
+ }
+ }
+ return self;
+}
+
+- (nullable id)nextObject {
+ if ([self.stack count] == 0) {
+ return nil;
+ }
+
+ id<FSTLLRBNode> node = [self.stack lastObject];
+ [self.stack removeLastObject];
+ id result = node.key;
+ NSComparator comparator = self.immutableSortedDictionary.comparator;
+
+ node = self.isReverse ? node.left : node.right;
+ while (![node isEmpty]) {
+ NSComparisonResult comparedToEnd = NSOrderedAscending;
+ if (self.endKey) {
+ comparedToEnd = comparator(node.key, self.endKey);
+ if (self.isReverse) {
+ comparedToEnd *= -1;
+ }
+ }
+ if (comparedToEnd == NSOrderedAscending) {
+ [self.stack addObject:node];
+ }
+ node = self.isReverse ? node.right : node.left;
+ }
+
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/LICENSE b/Firestore/third_party/Immutable/LICENSE
new file mode 100644
index 0000000..b437003
--- /dev/null
+++ b/Firestore/third_party/Immutable/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2012 Mads Hartmann Jensen
+
+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.
diff --git a/Firestore/third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m b/Firestore/third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m
new file mode 100644
index 0000000..68f2fc3
--- /dev/null
+++ b/Firestore/third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m
@@ -0,0 +1,467 @@
+#import "Immutable/FSTArraySortedDictionary.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Util/FSTAssert.h"
+
+@interface FSTArraySortedDictionary (Test)
+// Override methods to return subtype.
+- (FSTArraySortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey;
+- (FSTArraySortedDictionary *)dictionaryByRemovingObjectForKey:(id)aKey;
+@end
+
+@interface FSTArraySortedDictionaryTests : XCTestCase
+@end
+
+@implementation FSTArraySortedDictionaryTests
+
+- (NSComparator)defaultComparator {
+ return ^(id obj1, id obj2) {
+ FSTAssert([obj1 respondsToSelector:@selector(compare:)] &&
+ [obj2 respondsToSelector:@selector(compare:)],
+ @"Objects must support compare: %@ %@", obj1, obj2);
+ return [obj1 compare:obj2];
+ };
+}
+
+- (void)testSearchForSpecificKey {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+
+ XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found second object");
+ XCTAssertEqualObjects(map[@1], @1, @"Found first object");
+ XCTAssertEqualObjects(map[@2], @2, @"Found second object");
+ XCTAssertNil([map objectForKey:@3], @"Properly not found object");
+}
+
+- (void)testRemoveKeyValuePair {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+
+ FSTImmutableSortedDictionary *newMap = [map dictionaryByRemovingObjectForKey:@1];
+ XCTAssertEqualObjects([newMap objectForKey:@2], @2, @"Found second object");
+ XCTAssertNil([newMap objectForKey:@1], @"Properly not found object");
+
+ // Make sure the original one is not mutated
+ XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found second object");
+}
+
+- (void)testMoreRemovals {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@50 forKey:@50];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+ map = [map dictionaryBySettingObject:@7 forKey:@7];
+ map = [map dictionaryBySettingObject:@9 forKey:@9];
+ map = [map dictionaryBySettingObject:@1 forKey:@20];
+ map = [map dictionaryBySettingObject:@18 forKey:@18];
+ map = [map dictionaryBySettingObject:@3 forKey:@2];
+ map = [map dictionaryBySettingObject:@4 forKey:@71];
+ map = [map dictionaryBySettingObject:@7 forKey:@42];
+ map = [map dictionaryBySettingObject:@9 forKey:@88];
+
+ XCTAssertNotNil([map objectForKey:@7], @"Found object");
+ XCTAssertNotNil([map objectForKey:@3], @"Found object");
+ XCTAssertNotNil([map objectForKey:@1], @"Found object");
+
+ FSTImmutableSortedDictionary *m1 = [map dictionaryByRemovingObjectForKey:@7];
+ FSTImmutableSortedDictionary *m2 = [map dictionaryByRemovingObjectForKey:@3];
+ FSTImmutableSortedDictionary *m3 = [map dictionaryByRemovingObjectForKey:@1];
+
+ XCTAssertNil([m1 objectForKey:@7], @"Removed object");
+ XCTAssertNotNil([m1 objectForKey:@3], @"Found object");
+ XCTAssertNotNil([m1 objectForKey:@1], @"Found object");
+
+ XCTAssertNil([m2 objectForKey:@3], @"Removed object");
+ XCTAssertNotNil([m2 objectForKey:@7], @"Found object");
+ XCTAssertNotNil([m2 objectForKey:@1], @"Found object");
+
+ XCTAssertNil([m3 objectForKey:@1], @"Removed object");
+ XCTAssertNotNil([m3 objectForKey:@7], @"Found object");
+ XCTAssertNotNil([m3 objectForKey:@3], @"Found object");
+}
+
+- (void)testRemovalBug {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+
+ XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found object");
+ XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found object");
+ XCTAssertEqualObjects([map objectForKey:@3], @3, @"Found object");
+
+ FSTImmutableSortedDictionary *m1 = [map dictionaryByRemovingObjectForKey:@2];
+ XCTAssertEqualObjects([m1 objectForKey:@1], @1, @"Found object");
+ XCTAssertEqualObjects([m1 objectForKey:@3], @3, @"Found object");
+ XCTAssertNil([m1 objectForKey:@2], @"Removed object");
+}
+
+- (void)testIncreasing {
+ int total = 100;
+
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ for (int i = 0; i < total; i++) {
+ NSNumber *item = @(i);
+ map = [map dictionaryBySettingObject:item forKey:item];
+ }
+
+ XCTAssertTrue(map.count == 100, @"Check if all 100 objects are in the map");
+
+ for (int i = 0; i < total; i++) {
+ NSNumber *item = @(i);
+ map = [map dictionaryByRemovingObjectForKey:item];
+ }
+
+ XCTAssertTrue(map.count == 0, @"Check if all 100 objects were removed");
+}
+
+- (void)testOverride {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@10 forKey:@10];
+ map = [map dictionaryBySettingObject:@8 forKey:@10];
+
+ XCTAssertEqualObjects([map objectForKey:@10], @8, @"Found first object");
+}
+
+- (void)testEmpty {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@10 forKey:@10];
+ map = [map dictionaryByRemovingObjectForKey:@10];
+
+ XCTAssertTrue([map isEmpty], @"Properly empty");
+}
+
+- (void)testEmptyGet {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertNil([map objectForKey:@"something"], @"Properly nil");
+}
+
+- (void)testEmptyCount {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([map count] == 0, @"Properly zero count");
+}
+
+- (void)testEmptyRemoval {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryByRemovingObjectForKey:@"something"];
+ XCTAssertTrue(map.count == 0, @"Properly zero count");
+}
+
+- (void)testReverseTraversal {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@5 forKey:@5];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+
+ __block int next = 5;
+ [map enumerateKeysAndObjectsReverse:YES
+ usingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, @(next), @"Properly equal");
+ next = next - 1;
+ }];
+}
+
+- (void)testInsertionAndRemovalOfAHundredItems {
+ NSUInteger n = 100;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+ NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:n];
+
+ for (NSUInteger i = 0; i < n; i++) {
+ [toInsert addObject:@(i)];
+ [toRemove addObject:@(i)];
+ }
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, @(next), @"Correct key");
+ XCTAssertEqualObjects(value, @(next), @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, n, @"Check we traversed all of the items");
+
+ // remove them
+
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryByRemovingObjectForKey:toRemove[i]];
+ }
+
+ XCTAssertEqual(map.count, 0, @"Check we removed all of the items");
+}
+
+- (void)shuffleArray:(NSMutableArray *)array {
+ NSUInteger count = array.count;
+ for (NSUInteger i = 0; i < count; i++) {
+ NSUInteger nElements = count - i;
+ NSUInteger n = (arc4random() % nElements) + i;
+ [array exchangeObjectAtIndex:i withObjectAtIndex:n];
+ }
+}
+
+- (void)testBalanceProblem {
+ NSArray *toInsert = @[ @1, @7, @8, @5, @2, @6, @4, @0, @3 ];
+
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for (NSUInteger i = 0; i < toInsert.count; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == toInsert.count, @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, @(next), @"Correct key");
+ XCTAssertEqualObjects(value, @(next), @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual((int)next, (int)toInsert.count, @"Check we traversed all of the items");
+}
+
+- (void)testPredecessorKey {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@50 forKey:@50];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+ map = [map dictionaryBySettingObject:@7 forKey:@7];
+ map = [map dictionaryBySettingObject:@9 forKey:@9];
+
+ XCTAssertNil([map predecessorKey:@1], @"First object doesn't have a predecessor");
+ XCTAssertEqualObjects([map predecessorKey:@3], @1, @"@1");
+ XCTAssertEqualObjects([map predecessorKey:@4], @3, @"@3");
+ XCTAssertEqualObjects([map predecessorKey:@7], @4, @"@4");
+ XCTAssertEqualObjects([map predecessorKey:@9], @7, @"@7");
+ XCTAssertEqualObjects([map predecessorKey:@50], @9, @"@9");
+ XCTAssertThrows([map predecessorKey:@777], @"Expect exception about nonexistent key");
+}
+
+// This is a macro instead of a method so that the failures show on the proper lines.
+#define ASSERT_ENUMERATOR(enumerator, start, end, step) \
+ do { \
+ NSEnumerator *e = (enumerator); \
+ id next = nil; \
+ for (NSUInteger i = (start); i != (end); i += (step)) { \
+ next = [e nextObject]; \
+ XCTAssertNotNil(next, @"expected %lu. got nil.", (unsigned long)i); \
+ XCTAssertEqualObjects(next, @(i), "expected %lu. got %@.", (unsigned long)i, next); \
+ } \
+ next = [e nextObject]; \
+ XCTAssertNil(next, @"expected nil. got %@.", next); \
+ } while (0)
+
+- (void)testEnumerator {
+ NSUInteger n = 100;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+ NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:n];
+
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i)];
+ [toRemove addObject:@(i)];
+ }
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:self.defaultComparator];
+
+ // add them to the dictionary
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+
+ ASSERT_ENUMERATOR([map keyEnumerator], 0, 100, 1);
+}
+
+- (void)testReverseEnumerator {
+ NSUInteger n = 20;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i)];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FSTImmutableSortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // Add them to the dictionary.
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:FSTArraySortedDictionary.class],
+ @"Make sure we still have a array backed dictionary");
+
+ ASSERT_ENUMERATOR([map reverseKeyEnumerator], n - 1, -1, -1);
+}
+
+- (void)testEnumeratorFrom {
+ // Create a dictionary with the even numbers in [2, 42).
+ NSUInteger n = 20;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i * 2 + 2)];
+ }
+ [self shuffleArray:toInsert];
+
+ FSTImmutableSortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // Add them to the dictionary.
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:FSTArraySortedDictionary.class],
+ @"Make sure we still have a array backed dictionary");
+
+ // Test from before keys.
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0], 2, n * 2 + 2, 2);
+
+ // Test from after keys.
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100], 0, 0, 2);
+
+ // Test from key in map.
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@10], 10, n * 2 + 2, 2);
+
+ // Test from in between keys.
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@11], 12, n * 2 + 2, 2);
+}
+
+- (void)testEnumeratorFromTo {
+ // Create a dictionary with the even numbers in [2, 42).
+ NSUInteger n = 20;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i * 2 + 2)];
+ }
+ [self shuffleArray:toInsert];
+
+ FSTImmutableSortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // Add them to the dictionary.
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:FSTArraySortedDictionary.class],
+ @"Make sure we still have an array backed dictionary");
+
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@1], 2, 2, 2); // before to before
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@100], 2, n * 2 + 2, 2); // before to after
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@6], 2, 6, 2); // before to key in map
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@7], 2, 8, 2); // before to in between keys
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@0], 2, 2, 2); // after to before
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@110], 2, 2, 2); // after to after
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@6], 2, 2, 2); // after to key in map
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@7], 2, 2, 2); // after to in between
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@0], 6, 6, 2); // key in map to before
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@100], 6, n * 2 + 2, 2); // key in map to after
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@10], 6, 10, 2); // key in map to key in map
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@11], 6, 12, 2); // key in map to in between
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@0], 8, 8, 2); // in between to before
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@100], 8, n * 2 + 2, 2); // in between to after
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@10], 8, 10, 2); // in between to key in map
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@13], 8, 14, 2); // in between to in between
+}
+
+- (void)testReverseEnumeratorFrom {
+ NSUInteger n = 20;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i * 2 + 2)];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FSTImmutableSortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:FSTArraySortedDictionary.class],
+ @"Make sure we still have a array backed dictionary");
+
+ // Test from before keys.
+ ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@0], 0, 0, -2);
+
+ // Test from after keys.
+ ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@100], n * 2, 0, -2);
+
+ // Test from key in map.
+ ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@10], 10, 0, -2);
+
+ // Test from in between keys.
+ ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@11], 10, 0, -2);
+}
+
+#undef ASSERT_ENUMERATOR
+
+- (void)testIndexOf {
+ FSTArraySortedDictionary *map =
+ [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@50 forKey:@50];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+ map = [map dictionaryBySettingObject:@7 forKey:@7];
+ map = [map dictionaryBySettingObject:@9 forKey:@9];
+
+ XCTAssertEqual([map indexOfKey:@0], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@1], 0);
+ XCTAssertEqual([map indexOfKey:@2], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@3], 1);
+ XCTAssertEqual([map indexOfKey:@4], 2);
+ XCTAssertEqual([map indexOfKey:@5], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@6], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@7], 3);
+ XCTAssertEqual([map indexOfKey:@8], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@9], 4);
+ XCTAssertEqual([map indexOfKey:@50], 5);
+}
+
+@end
diff --git a/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h
new file mode 100644
index 0000000..7496173
--- /dev/null
+++ b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h
@@ -0,0 +1,17 @@
+#import <Foundation/Foundation.h>
+
+#import "Immutable/FSTImmutableSortedDictionary.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// clang-format doesn't yet deal with generic parameters and categories :-(
+// clang-format off
+@interface FSTImmutableSortedDictionary<KeyType, __covariant ValueType> (Testing)
+
+/** Converts the values of the dictionary to an array preserving order. */
+- (NSArray<ValueType> *)values;
+
+@end
+// clang-format on
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m
new file mode 100644
index 0000000..d402916
--- /dev/null
+++ b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m
@@ -0,0 +1,17 @@
+#import "FSTImmutableSortedDictionary+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTImmutableSortedDictionary (Testing)
+
+- (NSArray<id> *)values {
+ NSMutableArray<id> *result = [NSMutableArray array];
+ [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ [result addObject:value];
+ }];
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h
new file mode 100644
index 0000000..a0e25d7
--- /dev/null
+++ b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h
@@ -0,0 +1,20 @@
+#import "Immutable/FSTImmutableSortedSet.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// clang-format doesn't yet deal with generic parameters and categories :-(
+// clang-format off
+@interface FSTImmutableSortedSet<T> (Testing)
+
+/**
+ * An array containing the set’s members, or an empty array if the set has no members.
+ *
+ * Implemented here for compatibility with NSSet in testing though we'd never want to do this
+ * in production code.
+ */
+- (NSArray<T> *)allObjects;
+
+@end
+// clang-format on
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m
new file mode 100644
index 0000000..d4d81e0
--- /dev/null
+++ b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m
@@ -0,0 +1,17 @@
+#import "FSTImmutableSortedSet+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTImmutableSortedSet (Testing)
+
+- (NSArray<id> *)allObjects {
+ NSMutableArray<id> *result = [NSMutableArray array];
+ [self enumerateObjectsUsingBlock:^(id object, BOOL *stop) {
+ [result addObject:object];
+ }];
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/third_party/Immutable/Tests/FSTLLRBValueNode+Test.h b/Firestore/third_party/Immutable/Tests/FSTLLRBValueNode+Test.h
new file mode 100644
index 0000000..3b05647
--- /dev/null
+++ b/Firestore/third_party/Immutable/Tests/FSTLLRBValueNode+Test.h
@@ -0,0 +1,10 @@
+#import "Immutable/FSTLLRBValueNode.h"
+
+#import <Foundation/Foundation.h>
+
+/** Extra methods exposed only for testing. */
+@interface FSTLLRBValueNode (Test)
+- (id<FSTLLRBNode>)rotateLeft;
+- (id<FSTLLRBNode>)rotateRight;
+- (BOOL)checkMaxDepth;
+@end
diff --git a/Firestore/third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m b/Firestore/third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m
new file mode 100644
index 0000000..352ac4e
--- /dev/null
+++ b/Firestore/third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m
@@ -0,0 +1,655 @@
+#import <XCTest/XCTest.h>
+
+#import "Immutable/FSTLLRBEmptyNode.h"
+#import "Immutable/FSTLLRBNode.h"
+#import "Immutable/FSTLLRBValueNode.h"
+#import "Immutable/FSTTreeSortedDictionary.h"
+#import "Util/FSTAssert.h"
+
+#import "FSTLLRBValueNode+Test.h"
+
+@interface FSTTreeSortedDictionary (Test)
+// Override methods to return subtype.
+- (FSTTreeSortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey;
+- (FSTTreeSortedDictionary *)dictionaryByRemovingObjectForKey:(id)aKey;
+@end
+
+@interface FSTTreeSortedDictionaryTests : XCTestCase
+@end
+
+@implementation FSTTreeSortedDictionaryTests
+
+- (NSComparator)defaultComparator {
+ return ^(id obj1, id obj2) {
+ FSTAssert([obj1 respondsToSelector:@selector(compare:)] &&
+ [obj2 respondsToSelector:@selector(compare:)],
+ @"Objects must support compare: %@ %@", obj1, obj2);
+ return [obj1 compare:obj2];
+ };
+}
+
+- (void)testCreateNode {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@"value" forKey:@"key"];
+ XCTAssertTrue([map.root.left isEmpty], @"Left child is properly empty");
+ XCTAssertTrue([map.root.right isEmpty], @"Right child is properly empty");
+}
+
+- (void)testSearchForSpecificKey {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+
+ XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found second object");
+ XCTAssertEqualObjects(map[@1], @1, @"Found first object");
+ XCTAssertEqualObjects(map[@2], @2, @"Found second object");
+ XCTAssertNil([map objectForKey:@3], @"Properly not found object");
+}
+
+- (void)testInsertNewKeyValuePair {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+
+ XCTAssertEqualObjects(map.root.key, @2, @"Check the root key");
+ XCTAssertEqualObjects(map.root.left.key, @1, @"Check the root.left key");
+}
+
+- (void)testRemoveKeyValuePair {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+
+ FSTImmutableSortedDictionary *newMap = [map dictionaryByRemovingObjectForKey:@1];
+ XCTAssertEqualObjects([newMap objectForKey:@2], @2, @"Found second object");
+ XCTAssertNil([newMap objectForKey:@1], @"Properly not found object");
+
+ // Make sure the original one is not mutated
+ XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found second object");
+}
+
+- (void)testMoreRemovals {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@50 forKey:@50];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+ map = [map dictionaryBySettingObject:@7 forKey:@7];
+ map = [map dictionaryBySettingObject:@9 forKey:@9];
+ map = [map dictionaryBySettingObject:@1 forKey:@20];
+ map = [map dictionaryBySettingObject:@18 forKey:@18];
+ map = [map dictionaryBySettingObject:@3 forKey:@2];
+ map = [map dictionaryBySettingObject:@4 forKey:@71];
+ map = [map dictionaryBySettingObject:@7 forKey:@42];
+ map = [map dictionaryBySettingObject:@9 forKey:@88];
+
+ XCTAssertNotNil([map objectForKey:@7], @"Found object");
+ XCTAssertNotNil([map objectForKey:@3], @"Found object");
+ XCTAssertNotNil([map objectForKey:@1], @"Found object");
+
+ FSTImmutableSortedDictionary *m1 = [map dictionaryByRemovingObjectForKey:@7];
+ FSTImmutableSortedDictionary *m2 = [map dictionaryByRemovingObjectForKey:@3];
+ FSTImmutableSortedDictionary *m3 = [map dictionaryByRemovingObjectForKey:@1];
+
+ XCTAssertNil([m1 objectForKey:@7], @"Removed object");
+ XCTAssertNotNil([m1 objectForKey:@3], @"Found object");
+ XCTAssertNotNil([m1 objectForKey:@1], @"Found object");
+
+ XCTAssertNil([m2 objectForKey:@3], @"Removed object");
+ XCTAssertNotNil([m2 objectForKey:@7], @"Found object");
+ XCTAssertNotNil([m2 objectForKey:@1], @"Found object");
+
+ XCTAssertNil([m3 objectForKey:@1], @"Removed object");
+ XCTAssertNotNil([m3 objectForKey:@7], @"Found object");
+ XCTAssertNotNil([m3 objectForKey:@3], @"Found object");
+}
+
+- (void)testRemovalBug {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+
+ XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found object");
+ XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found object");
+ XCTAssertEqualObjects([map objectForKey:@3], @3, @"Found object");
+
+ FSTImmutableSortedDictionary *m1 = [map dictionaryByRemovingObjectForKey:@2];
+ XCTAssertEqualObjects([m1 objectForKey:@1], @1, @"Found object");
+ XCTAssertEqualObjects([m1 objectForKey:@3], @3, @"Found object");
+ XCTAssertNil([m1 objectForKey:@2], @"Removed object");
+}
+
+- (void)testIncreasing {
+ int total = 100;
+
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ for (int i = 0; i < total; i++) {
+ NSNumber *item = @(i);
+ map = [map dictionaryBySettingObject:item forKey:item];
+ }
+
+ XCTAssertTrue(map.count == 100, @"Check if all 100 objects are in the map");
+ XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+
+ for (int i = 0; i < total; i++) {
+ NSNumber *item = @(i);
+ map = [map dictionaryByRemovingObjectForKey:item];
+ }
+
+ XCTAssertTrue(map.count == 0, @"Check if all 100 objects were removed");
+ // We can't check the depth here because the map no longer contains values, so we check that it
+ // doesn't respond to this check
+ XCTAssertTrue([map.root isMemberOfClass:FSTLLRBEmptyNode.class], @"Root is an empty node");
+ XCTAssertFalse([map respondsToSelector:@selector(checkMaxDepth)],
+ @"The empty node doesn't respond to this selector.");
+}
+
+- (void)testStructureShouldBeValidAfterInsertionA {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+
+ XCTAssertEqualObjects(map.root.key, @2, @"Check root key");
+ XCTAssertEqualObjects(map.root.left.key, @1, @"Check the left key is correct");
+ XCTAssertEqualObjects(map.root.right.key, @3, @"Check the right key is correct");
+}
+
+- (void)testStructureShouldBeValidAfterInsertionB {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@50 forKey:@50];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+ map = [map dictionaryBySettingObject:@7 forKey:@7];
+ map = [map dictionaryBySettingObject:@9 forKey:@9];
+ map = [map dictionaryBySettingObject:@1 forKey:@20];
+ map = [map dictionaryBySettingObject:@18 forKey:@18];
+ map = [map dictionaryBySettingObject:@3 forKey:@2];
+ map = [map dictionaryBySettingObject:@4 forKey:@71];
+ map = [map dictionaryBySettingObject:@7 forKey:@42];
+ map = [map dictionaryBySettingObject:@9 forKey:@88];
+
+ XCTAssertTrue(map.count == 12, @"Check if all 12 objects are in the map");
+ XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+}
+
+- (void)testRotateLeftLeavesTreeInAValidState {
+ FSTLLRBValueNode *node = [[FSTLLRBValueNode alloc]
+ initWithKey:@4
+ withValue:@4
+ withColor:FSTLLRBColorBlack
+ withLeft:[[FSTLLRBValueNode alloc] initWithKey:@2
+ withValue:@2
+ withColor:FSTLLRBColorBlack
+ withLeft:nil
+ withRight:nil]
+ withRight:[[FSTLLRBValueNode alloc]
+ initWithKey:@7
+ withValue:@7
+ withColor:FSTLLRBColorRed
+ withLeft:[[FSTLLRBValueNode alloc] initWithKey:@5
+ withValue:@5
+ withColor:FSTLLRBColorBlack
+ withLeft:nil
+ withRight:nil]
+ withRight:[[FSTLLRBValueNode alloc] initWithKey:@8
+ withValue:@8
+ withColor:FSTLLRBColorBlack
+ withLeft:nil
+ withRight:nil]]];
+
+ FSTLLRBValueNode *node2 = [node rotateLeft];
+
+ XCTAssertTrue(node2.count == 5, @"Make sure the count is correct");
+ XCTAssertTrue([node2 checkMaxDepth], @"Check proper structure");
+}
+
+- (void)testRotateRightLeavesTreeInAValidState {
+ FSTLLRBValueNode *node = [[FSTLLRBValueNode alloc]
+ initWithKey:@7
+ withValue:@7
+ withColor:FSTLLRBColorBlack
+ withLeft:[[FSTLLRBValueNode alloc]
+ initWithKey:@4
+ withValue:@4
+ withColor:FSTLLRBColorRed
+ withLeft:[[FSTLLRBValueNode alloc] initWithKey:@2
+ withValue:@2
+ withColor:FSTLLRBColorBlack
+ withLeft:nil
+ withRight:nil]
+ withRight:[[FSTLLRBValueNode alloc] initWithKey:@5
+ withValue:@5
+ withColor:FSTLLRBColorBlack
+ withLeft:nil
+ withRight:nil]]
+ withRight:[[FSTLLRBValueNode alloc] initWithKey:@8
+ withValue:@8
+ withColor:FSTLLRBColorBlack
+ withLeft:nil
+ withRight:nil]];
+
+ FSTLLRBValueNode *node2 = [node rotateRight];
+ XCTAssertTrue(node2.count == 5, @"Make sure the count is correct");
+ XCTAssertEqualObjects(node2.key, @4, @"Check roots key");
+ XCTAssertEqualObjects(node2.left.key, @2, @"Check first left child key");
+ XCTAssertEqualObjects(node2.right.key, @7, @"Check first right child key");
+ XCTAssertEqualObjects(node2.right.left.key, @5, @"Check second right left key");
+ XCTAssertEqualObjects(node2.right.right.key, @8, @"Check second right left key");
+}
+
+- (void)testStructureShouldBeValidAfterInsertionC {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@50 forKey:@50];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+ map = [map dictionaryBySettingObject:@7 forKey:@7];
+ map = [map dictionaryBySettingObject:@9 forKey:@9];
+
+ XCTAssertTrue(map.count == 6, @"Check if all 6 objects are in the map");
+ XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+
+ FSTTreeSortedDictionary *m2 = map;
+ m2 = [m2 dictionaryBySettingObject:@20 forKey:@20];
+ m2 = [m2 dictionaryBySettingObject:@18 forKey:@18];
+ m2 = [m2 dictionaryBySettingObject:@2 forKey:@2];
+
+ XCTAssertTrue(m2.count == 9, @"Check if all 9 objects are in the map");
+ XCTAssertTrue([m2.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)m2.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+
+ FSTTreeSortedDictionary *m3 = m2;
+ m3 = [m3 dictionaryBySettingObject:@71 forKey:@71];
+ m3 = [m3 dictionaryBySettingObject:@42 forKey:@42];
+ m3 = [m3 dictionaryBySettingObject:@88 forKey:@88];
+ m3 = [m3 dictionaryBySettingObject:@20 forKey:@20]; // Add a dupe to see if the size is correct
+
+ XCTAssertTrue(m3.count == 12, @"Check if all 12 (minus dupe @20) objects are in the map");
+ XCTAssertTrue([m3.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)m3.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+}
+
+- (void)testOverride {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@10 forKey:@10];
+ map = [map dictionaryBySettingObject:@8 forKey:@10];
+
+ XCTAssertEqualObjects([map objectForKey:@10], @8, @"Found first object");
+}
+- (void)testEmpty {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@10 forKey:@10];
+ map = [map dictionaryByRemovingObjectForKey:@10];
+
+ XCTAssertTrue([map isEmpty], @"Properly empty");
+}
+
+- (void)testEmptyGet {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertNil([map objectForKey:@"something"], @"Properly nil");
+}
+
+- (void)testEmptyCount {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([map count] == 0, @"Properly zero count");
+}
+
+- (void)testEmptyRemoval {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryByRemovingObjectForKey:@"something"];
+ XCTAssertTrue(map.count == 0, @"Properly zero count");
+}
+
+- (void)testReverseTraversal {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@5 forKey:@5];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@2 forKey:@2];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+
+ __block int next = 5;
+ [map enumerateKeysAndObjectsReverse:YES
+ usingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, @(next), @"Properly equal");
+ next = next - 1;
+ }];
+}
+
+- (void)testInsertionAndRemovalOfAHundredItems {
+ NSUInteger n = 100;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+ NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:n];
+
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i)];
+ [toRemove addObject:@(i)];
+ }
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, @(next), @"Correct key");
+ XCTAssertEqualObjects(value, @(next), @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, n, @"Check we traversed all of the items");
+
+ // remove them
+
+ for (NSUInteger i = 0; i < n; i++) {
+ if ([map.root isMemberOfClass:FSTLLRBValueNode.class]) {
+ XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+ }
+ map = [map dictionaryByRemovingObjectForKey:toRemove[i]];
+ }
+
+ XCTAssertEqual(map.count, 0, @"Check we removed all of the items");
+}
+
+- (void)shuffleArray:(NSMutableArray *)array {
+ NSUInteger count = array.count;
+ for (NSUInteger i = 0; i < count; i++) {
+ NSUInteger nElements = count - i;
+ NSUInteger n = (arc4random() % nElements) + i;
+ [array exchangeObjectAtIndex:i withObjectAtIndex:n];
+ }
+}
+
+- (void)testBalanceProblem {
+ NSArray *toInsert = @[ @1, @7, @8, @5, @2, @6, @4, @0, @3 ];
+
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for (NSUInteger i = 0; i < toInsert.count; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+ }
+ XCTAssertTrue(map.count == toInsert.count, @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, @(next), @"Correct key");
+ XCTAssertEqualObjects(value, @(next), @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual((int)next, (int)toInsert.count, @"Check we traversed all of the items");
+
+ // removing one triggers the balance problem
+ map = [map dictionaryByRemovingObjectForKey:@5];
+
+ if ([map.root isMemberOfClass:FSTLLRBValueNode.class]) {
+ XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+ }
+}
+
+- (void)testPredecessorKey {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@50 forKey:@50];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+ map = [map dictionaryBySettingObject:@7 forKey:@7];
+ map = [map dictionaryBySettingObject:@9 forKey:@9];
+
+ XCTAssertNil([map predecessorKey:@1], @"First object doesn't have a predecessor");
+ XCTAssertEqualObjects([map predecessorKey:@3], @1, @"@1");
+ XCTAssertEqualObjects([map predecessorKey:@4], @3, @"@3");
+ XCTAssertEqualObjects([map predecessorKey:@7], @4, @"@4");
+ XCTAssertEqualObjects([map predecessorKey:@9], @7, @"@7");
+ XCTAssertEqualObjects([map predecessorKey:@50], @9, @"@9");
+ XCTAssertThrows([map predecessorKey:@777], @"Expect exception about nonexistent key");
+}
+
+// This is a macro instead of a method so that the failures show on the proper lines.
+#define ASSERT_ENUMERATOR(enumerator, start, end, step) \
+ do { \
+ NSEnumerator *e = (enumerator); \
+ id next = nil; \
+ for (NSUInteger i = (start); i != (end); i += (step)) { \
+ next = [e nextObject]; \
+ XCTAssertNotNil(next, @"expected %lu. got nil.", (unsigned long)i); \
+ XCTAssertEqualObjects(next, @(i), "expected %lu. got %@.", (unsigned long)i, next); \
+ } \
+ next = [e nextObject]; \
+ XCTAssertNil(next, @"expected nil. got %@.", next); \
+ } while (0)
+
+- (void)testEnumerator {
+ NSUInteger n = 100;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+ NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:n];
+
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i)];
+ [toRemove addObject:@(i)];
+ }
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:self.defaultComparator];
+
+ // add them to the dictionary
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node");
+ XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth],
+ @"Checking valid depth and tree structure");
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+
+ ASSERT_ENUMERATOR([map keyEnumerator], 0, 100, 1);
+}
+
+- (void)testReverseEnumerator {
+ NSUInteger n = 20;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i)];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FSTImmutableSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // Add them to the dictionary.
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:FSTTreeSortedDictionary.class],
+ @"Make sure we still have a tree backed dictionary");
+
+ ASSERT_ENUMERATOR([map reverseKeyEnumerator], n - 1, -1, -1);
+}
+
+- (void)testEnumeratorFrom {
+ // Create a dictionary with the even numbers in [2, 42).
+ NSUInteger n = 20;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i * 2 + 2)];
+ }
+ [self shuffleArray:toInsert];
+
+ FSTImmutableSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // Add them to the dictionary.
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:FSTTreeSortedDictionary.class],
+ @"Make sure we still have a tree backed dictionary");
+
+ // Test from before keys.
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0], 2, n * 2 + 2, 2);
+
+ // Test from after keys.
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100], 0, 0, 2);
+
+ // Test from key in map.
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@10], 10, n * 2 + 2, 2);
+
+ // Test from in between keys.
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@11], 12, n * 2 + 2, 2);
+}
+
+- (void)testEnumeratorFromTo {
+ // Create a dictionary with the even numbers in [2, 42).
+ NSUInteger n = 20;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i * 2 + 2)];
+ }
+ [self shuffleArray:toInsert];
+
+ FSTImmutableSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // Add them to the dictionary.
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:FSTTreeSortedDictionary.class],
+ @"Make sure we still have a tree backed dictionary");
+
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@1], 2, 2, 2); // before to before
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@100], 2, n * 2 + 2, 2); // before to after
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@6], 2, 6, 2); // before to key in map
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@7], 2, 8, 2); // before to in between keys
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@0], 2, 2, 2); // after to before
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@110], 2, 2, 2); // after to after
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@6], 2, 2, 2); // after to key in map
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@7], 2, 2, 2); // after to in between
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@0], 6, 6, 2); // key in map to before
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@100], 6, n * 2 + 2, 2); // key in map to after
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@10], 6, 10, 2); // key in map to key in map
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@11], 6, 12, 2); // key in map to in between
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@0], 8, 8, 2); // in between to before
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@100], 8, n * 2 + 2, 2); // in between to after
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@10], 8, 10, 2); // in between to key in map
+ ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@13], 8, 14, 2); // in between to in between
+}
+
+- (void)testReverseEnumeratorFrom {
+ NSUInteger n = 20;
+ NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n];
+
+ for (int i = 0; i < n; i++) {
+ [toInsert addObject:@(i * 2 + 2)];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FSTImmutableSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for (NSUInteger i = 0; i < n; i++) {
+ map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]];
+ }
+ XCTAssertTrue(map.count == n, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:FSTTreeSortedDictionary.class],
+ @"Make sure we still have a tree backed dictionary");
+
+ // Test from before keys.
+ ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@0], 0, 0, -2);
+
+ // Test from after keys.
+ ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@100], n * 2, 0, -2);
+
+ // Test from key in map.
+ ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@10], 10, 0, -2);
+
+ // Test from in between keys.
+ ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@11], 10, 0, -2);
+}
+
+#undef ASSERT_ENUMERATOR
+
+- (void)testIndexOf {
+ FSTTreeSortedDictionary *map =
+ [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ map = [map dictionaryBySettingObject:@1 forKey:@1];
+ map = [map dictionaryBySettingObject:@50 forKey:@50];
+ map = [map dictionaryBySettingObject:@3 forKey:@3];
+ map = [map dictionaryBySettingObject:@4 forKey:@4];
+ map = [map dictionaryBySettingObject:@7 forKey:@7];
+ map = [map dictionaryBySettingObject:@9 forKey:@9];
+
+ XCTAssertEqual([map indexOfKey:@0], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@1], 0);
+ XCTAssertEqual([map indexOfKey:@2], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@3], 1);
+ XCTAssertEqual([map indexOfKey:@4], 2);
+ XCTAssertEqual([map indexOfKey:@5], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@6], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@7], 3);
+ XCTAssertEqual([map indexOfKey:@8], NSNotFound);
+ XCTAssertEqual([map indexOfKey:@9], 4);
+ XCTAssertEqual([map indexOfKey:@50], 5);
+}
+
+@end
diff --git a/README.md b/README.md
index 780a73f..fbc7fa8 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# Firebase iOS Open Source Development [![Build Status](https://travis-ci.org/firebase/firebase-ios-sdk.svg?branch=master)](https://travis-ci.org/firebase/firebase-ios-sdk)
This repository contains a subset of the Firebase iOS SDK source. It currently
-includes FirebaseCore, FirebaseAuth, FirebaseDatabase, FirebaseMessaging, and
-FirebaseStorage.
+includes FirebaseCore, FirebaseAuth, FirebaseDatabase, FirebaseMessaging,
+FirebaseStorage, and Firestore.
Firebase is an app development platform with tools to help you build, grow and
monetize your app. More information about Firebase can be found at
@@ -18,6 +18,10 @@ unit tests, integration tests, and reference samples.
Note, however, that the resulting FirebaseCommunity pod is NOT interoperable with the
official Firebase release pods because of different pod dependency definitions.
+Firestore has not yet been integrated with FirebaseCommunity. In the
+meantime, it has a self contained Xcode project. See
+[Firestore/README.md](Firestore/README.md).
+
Instructions and a script to build replaceable static library
frameworks at [BuildFrameworks](BuildFrameworks). The
resulting frameworks can be used to replace frameworks delivered by CocoaPods or
diff --git a/scripts/style.sh b/scripts/style.sh
index 1948188..3c2ac10 100755
--- a/scripts/style.sh
+++ b/scripts/style.sh
@@ -22,4 +22,5 @@ find . \
-name 'Pods' -prune -o \
-name '*.[mh]' \
-not -name '*.pbobjc.*' \
+ -not -name '*.pbrpc.*' \
-print0 | xargs -0 clang-format -style=file -i
diff --git a/test.sh b/test.sh
index 370a6b7..627fbad 100755
--- a/test.sh
+++ b/test.sh
@@ -52,4 +52,7 @@ if [ $RESULT == 65 ]; then
test_macOS; RESULT=$?
fi
-exit $RESULT
+if [ $RESULT != 0 ]; then exit $RESULT; fi
+
+# Also test Firestore
+Firestore/test.sh