diff options
Diffstat (limited to 'Firestore')
121 files changed, 5084 insertions, 1119 deletions
diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 5543325..c97aa23 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -24,6 +24,8 @@ Query.getDocuments() should fetch from server only, cache only, or attempt server and fall back to the cache (which was the only option previously, and is now the default.) +- [feature] Added new `mergeFields:(NSArray<id>*)` override for `set()` + which allows merging of a reduced subset of fields. # v0.11.0 - [fixed] Fixed a regression in the Firebase iOS SDK release 4.11.0 that could diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 8aecc9f..6738e74 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -27,7 +27,9 @@ 132E3E53179DE287D875F3F2 /* FSTLevelDBTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */; }; 347FDC6AA737A754541F7C8A /* Pods_Firestore_Tests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8525646842C83F703237BAA4 /* Pods_Firestore_Tests_iOS.framework */; }; 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; + 3DE7ABABD726C80991971BE1 /* Pods_Firestore_SwiftTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44B1394B81D5FCA818943A06 /* Pods_Firestore_SwiftTests_iOS.framework */; }; 5436F32420008FAD006E51E3 /* string_printf_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5436F32320008FAD006E51E3 /* string_printf_test.cc */; }; + 54511E8E209805F8005BD28F /* hashing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54511E8D209805F8005BD28F /* hashing_test.cc */; }; 5467FB01203E5717009C9584 /* FIRFirestoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FAFF203E56F8009C9584 /* FIRFirestoreTests.mm */; }; 5467FB08203E6A44009C9584 /* app_testing.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FB07203E6A44009C9584 /* app_testing.mm */; }; 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A531FC913E500713A1A /* secure_random_test.cc */; }; @@ -155,18 +157,21 @@ ABC1D7E42024AFDE00BA84F0 /* firebase_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = ABC1D7E22023CDC500BA84F0 /* firebase_credentials_provider_test.mm */; }; ABE6637A201FA81900ED349A /* database_id_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB71064B201FA60300344F18 /* database_id_test.cc */; }; ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; }; - AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; }; B6152AD7202A53CB000E5744 /* document_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6152AD5202A5385000E5744 /* document_key_test.cc */; }; B65D34A9203C995B0076A5E1 /* FIRTimestampTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */; }; B686F2AF2023DDEE0028D6BE /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; }; B686F2B22025000D0028D6BE /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; }; + B6FB467D208E9D3C00554BA2 /* async_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB467B208E9A8200554BA2 /* async_queue_test.cc */; }; + B6FB4684208EA0EC00554BA2 /* async_queue_test_libdispatch.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4680208EA0BE00554BA2 /* async_queue_test_libdispatch.cc */; }; + B6FB4685208EA0F000554BA2 /* async_queue_test_std.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4681208EA0BE00554BA2 /* async_queue_test_std.cc */; }; + B6FB468E208F9BAB00554BA2 /* executor_libdispatch_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.cc */; }; + B6FB468F208F9BAE00554BA2 /* executor_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4687208F9B9100554BA2 /* executor_std_test.cc */; }; + B6FB4690208F9BB300554BA2 /* executor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4688208F9B9100554BA2 /* executor_test.cc */; }; C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; }; CF08376B68945A0BB332D0C8 /* Pods_Firestore_IntegrationTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4EEE10E8E59CC91309335CA /* Pods_Firestore_IntegrationTests_iOS.framework */; }; - D9AF2279747DE7213156646C /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E5F50EF80014608B1868944 /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */; }; 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 */; }; DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */ = {isa = PBXBuildFile; fileRef = DE03B3621F215E1600A30B9C /* CAcert.pem */; }; DE0761F81F2FE68D003233AF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0761F61F2FE68D003233AF /* main.swift */; }; @@ -237,12 +242,15 @@ 245812330F6A31632BB4B623 /* Pods_Firestore_Example_Firestore_SwiftTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_Firestore_SwiftTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 24A6BEC38BF31BC4BF0E9DA7 /* Pods_Firestore_Example_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftBuildTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 39F1102E452A53A1F93AAA1F /* Pods-Firestore_SwiftTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_SwiftTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_SwiftTests_iOS/Pods-Firestore_SwiftTests_iOS.release.xcconfig"; sourceTree = "<group>"; }; 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = remote_store_spec_test.json; sourceTree = "<group>"; }; 3C7CE22C50805C4A854C73A1 /* Pods-Firestore_IntegrationTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS.debug.xcconfig"; sourceTree = "<group>"; }; 3F422FFBDA6E79396E2FB594 /* Pods-Firestore_Example-Firestore_SwiftTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example-Firestore_SwiftTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example-Firestore_SwiftTests_iOS/Pods-Firestore_Example-Firestore_SwiftTests_iOS.debug.xcconfig"; 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>"; }; + 44B1394B81D5FCA818943A06 /* Pods_Firestore_SwiftTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_SwiftTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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>"; }; 5436F32320008FAD006E51E3 /* string_printf_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = string_printf_test.cc; path = ../../core/test/firebase/firestore/util/string_printf_test.cc; sourceTree = "<group>"; }; + 54511E8D209805F8005BD28F /* hashing_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = hashing_test.cc; path = ../../core/test/firebase/firestore/util/hashing_test.cc; sourceTree = "<group>"; }; 5467FAFF203E56F8009C9584 /* FIRFirestoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFirestoreTests.mm; sourceTree = "<group>"; }; 5467FB06203E6A44009C9584 /* app_testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = app_testing.h; path = ../../core/test/firebase/firestore/testutil/app_testing.h; sourceTree = "<group>"; }; 5467FB07203E6A44009C9584 /* app_testing.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = app_testing.mm; path = ../../core/test/firebase/firestore/testutil/app_testing.mm; sourceTree = "<group>"; }; @@ -365,6 +373,7 @@ 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFirestoreSourceTests.mm; sourceTree = "<group>"; }; 618AC3C38A174084B9420162 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; sourceTree = "<group>"; }; + 635C1D9B5E36BC4C12A35E70 /* Pods-Firestore_SwiftTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_SwiftTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_SwiftTests_iOS/Pods-Firestore_SwiftTests_iOS.debug.xcconfig"; 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>"; }; 7346E61C20325C6900FD6CEF /* FSTDispatchQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDispatchQueueTests.mm; sourceTree = "<group>"; }; @@ -401,6 +410,15 @@ B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRTimestampTest.m; sourceTree = "<group>"; }; B686F2AD2023DDB20028D6BE /* field_path_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = field_path_test.cc; sourceTree = "<group>"; }; B686F2B02024FFD70028D6BE /* resource_path_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = resource_path_test.cc; sourceTree = "<group>"; }; + B6FB467A208E9A8200554BA2 /* async_queue_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = async_queue_test.h; path = ../../core/test/firebase/firestore/util/async_queue_test.h; sourceTree = "<group>"; }; + B6FB467B208E9A8200554BA2 /* async_queue_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = async_queue_test.cc; path = ../../core/test/firebase/firestore/util/async_queue_test.cc; sourceTree = "<group>"; }; + B6FB4680208EA0BE00554BA2 /* async_queue_test_libdispatch.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = async_queue_test_libdispatch.cc; path = ../../core/test/firebase/firestore/util/async_queue_test_libdispatch.cc; sourceTree = "<group>"; }; + B6FB4681208EA0BE00554BA2 /* async_queue_test_std.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = async_queue_test_std.cc; path = ../../core/test/firebase/firestore/util/async_queue_test_std.cc; sourceTree = "<group>"; }; + B6FB4686208F9B9100554BA2 /* async_tests_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = async_tests_util.h; path = ../../core/test/firebase/firestore/util/async_tests_util.h; sourceTree = "<group>"; }; + B6FB4687208F9B9100554BA2 /* executor_std_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = executor_std_test.cc; path = ../../core/test/firebase/firestore/util/executor_std_test.cc; sourceTree = "<group>"; }; + B6FB4688208F9B9100554BA2 /* executor_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = executor_test.cc; path = ../../core/test/firebase/firestore/util/executor_test.cc; sourceTree = "<group>"; }; + B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = executor_libdispatch_test.cc; path = ../../core/test/firebase/firestore/util/executor_libdispatch_test.cc; sourceTree = "<group>"; }; + B6FB468A208F9B9100554BA2 /* executor_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = executor_test.h; path = ../../core/test/firebase/firestore/util/executor_test.h; sourceTree = "<group>"; }; BE88081EE627C46349C918EF /* Pods-Firestore_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.debug.xcconfig"; sourceTree = "<group>"; }; C1D89E5405935366C88CC3E5 /* Pods-Firestore_Example-Firestore_SwiftTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example-Firestore_SwiftTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example-Firestore_SwiftTests_iOS/Pods-Firestore_Example-Firestore_SwiftTests_iOS.release.xcconfig"; sourceTree = "<group>"; }; 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>"; }; @@ -431,7 +449,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D9AF2279747DE7213156646C /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework in Frameworks */, + 3DE7ABABD726C80991971BE1 /* Pods_Firestore_SwiftTests_iOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -464,8 +482,6 @@ 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 */, CF08376B68945A0BB332D0C8 /* Pods_Firestore_IntegrationTests_iOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -493,10 +509,20 @@ 54740A561FC913EB00713A1A /* util */ = { isa = PBXGroup; children = ( + B6FB4686208F9B9100554BA2 /* async_tests_util.h */, + B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.cc */, + B6FB4687208F9B9100554BA2 /* executor_std_test.cc */, + B6FB4688208F9B9100554BA2 /* executor_test.cc */, + B6FB468A208F9B9100554BA2 /* executor_test.h */, + B6FB4680208EA0BE00554BA2 /* async_queue_test_libdispatch.cc */, + B6FB4681208EA0BE00554BA2 /* async_queue_test_std.cc */, + B6FB467B208E9A8200554BA2 /* async_queue_test.cc */, + B6FB467A208E9A8200554BA2 /* async_queue_test.h */, 548DB926200D590300E00ABC /* assert_test.cc */, 54740A521FC913E500713A1A /* autoid_test.cc */, AB380D01201BC69F00D97691 /* bits_test.cc */, 548DB928200D59F600E00ABC /* comparison_test.cc */, + 54511E8D209805F8005BD28F /* hashing_test.cc */, 54C2294E1FECABAE007D065B /* log_test.cc */, AB380D03201BC6E400D97691 /* ordered_code_test.cc */, 54740A531FC913E500713A1A /* secure_random_test.cc */, @@ -600,6 +626,7 @@ 0E5F50EF80014608B1868944 /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */, 8525646842C83F703237BAA4 /* Pods_Firestore_Tests_iOS.framework */, B4EEE10E8E59CC91309335CA /* Pods_Firestore_IntegrationTests_iOS.framework */, + 44B1394B81D5FCA818943A06 /* Pods_Firestore_SwiftTests_iOS.framework */, ); name = Frameworks; sourceTree = "<group>"; @@ -688,6 +715,8 @@ 5A3E3BE5F322D66EE3D6CB65 /* Pods-Firestore_Tests_iOS.release.xcconfig */, 3C7CE22C50805C4A854C73A1 /* Pods-Firestore_IntegrationTests_iOS.debug.xcconfig */, 618AC3C38A174084B9420162 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */, + 635C1D9B5E36BC4C12A35E70 /* Pods-Firestore_SwiftTests_iOS.debug.xcconfig */, + 39F1102E452A53A1F93AAA1F /* Pods-Firestore_SwiftTests_iOS.release.xcconfig */, ); name = Pods; sourceTree = "<group>"; @@ -958,7 +987,6 @@ 6003F586195388D20070C39A /* Sources */, 6003F587195388D20070C39A /* Frameworks */, 6003F588195388D20070C39A /* Resources */, - 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -997,7 +1025,7 @@ DE03B2981F2149D600A30B9C /* Sources */, DE03B2D31F2149D600A30B9C /* Frameworks */, DE03B2D81F2149D600A30B9C /* Resources */, - DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */, + A677B831B09F9BD04DC6DF32 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1189,50 +1217,32 @@ ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Firestore_SwiftTests_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; }; - 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */ = { + 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.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", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Check Pods Manifest.lock"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.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", + "$(DERIVED_FILE_DIR)/Pods-Firestore_Tests_iOS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS-frameworks.sh\"\n"; + 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; }; - 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */ = { + 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1243,38 +1253,56 @@ ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Firestore_Tests_iOS-checkManifestLockResult.txt", + "$(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; }; - 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */ = { + 9E2D564AC55ADE2D52B7E951 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_SwiftTests_iOS/Pods-Firestore_SwiftTests_iOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.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] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-SwiftBuildTest-checkManifestLockResult.txt", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.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 = "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"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_SwiftTests_iOS/Pods-Firestore_SwiftTests_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 9E2D564AC55ADE2D52B7E951 /* [CP] Embed Pods Frameworks */ = { + A677B831B09F9BD04DC6DF32 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh", + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS-frameworks.sh", "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", @@ -1301,7 +1329,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */ = { @@ -1311,13 +1339,31 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.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", "${BUILT_PRODUCTS_DIR}/GoogleTest/GoogleTest.framework", "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.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", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleTest.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", ); @@ -1344,24 +1390,6 @@ 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_iOS/Pods-Firestore_IntegrationTests_iOS-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_iOS/Pods-Firestore_IntegrationTests_iOS-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1429,12 +1457,15 @@ DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */, B686F2AF2023DDEE0028D6BE /* field_path_test.cc in Sources */, 5492E03120213FFC00B64F25 /* FSTLevelDBSpecTests.mm in Sources */, + B6FB468F208F9BAE00554BA2 /* executor_std_test.cc in Sources */, 5492E0B12021552D00B64F25 /* FSTRemoteDocumentCacheTests.mm in Sources */, 5492E0BA2021555100B64F25 /* FSTDocumentSetTests.mm in Sources */, 54740A581FC914F000713A1A /* autoid_test.cc in Sources */, 548DB927200D590300E00ABC /* assert_test.cc in Sources */, 5492E0A62021552D00B64F25 /* FSTPersistenceTestHelpers.mm in Sources */, + B6FB468E208F9BAB00554BA2 /* executor_libdispatch_test.cc in Sources */, 5467FB01203E5717009C9584 /* FIRFirestoreTests.mm in Sources */, + B6FB4684208EA0EC00554BA2 /* async_queue_test_libdispatch.cc in Sources */, 5492E0A12021552D00B64F25 /* FSTMemoryLocalStoreTests.mm in Sources */, 5436F32420008FAD006E51E3 /* string_printf_test.cc in Sources */, 5492E067202154B900B64F25 /* FSTEventManagerTests.mm in Sources */, @@ -1458,6 +1489,7 @@ 5492E09D2021552D00B64F25 /* FSTLocalStoreTests.mm in Sources */, 5492E0A32021552D00B64F25 /* FSTLocalSerializerTests.mm in Sources */, 5492E0A72021552D00B64F25 /* FSTLevelDBKeyTests.mm in Sources */, + B6FB4685208EA0F000554BA2 /* async_queue_test_std.cc in Sources */, 5492E0A22021552D00B64F25 /* FSTQueryCacheTests.mm in Sources */, 5492E0A52021552D00B64F25 /* FSTMemoryRemoteDocumentCacheTests.mm in Sources */, AB6B908820322E8800CC290A /* no_document_test.cc in Sources */, @@ -1467,6 +1499,7 @@ 5492E0C82021557E00B64F25 /* FSTDatastoreTests.mm in Sources */, 54995F6F205B6E12004EFFA0 /* leveldb_key_test.cc in Sources */, 5492E065202154B900B64F25 /* FSTViewTests.mm in Sources */, + B6FB467D208E9D3C00554BA2 /* async_queue_test.cc in Sources */, 5492E03C2021401F00B64F25 /* XCTestCase+Await.mm in Sources */, B6152AD7202A53CB000E5744 /* document_key_test.cc in Sources */, 5467FB08203E6A44009C9584 /* app_testing.mm in Sources */, @@ -1481,6 +1514,7 @@ ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */, 5492E0AE2021552D00B64F25 /* FSTLevelDBQueryCacheTests.mm in Sources */, ABC1D7DC2023A04B00BA84F0 /* credentials_provider_test.cc in Sources */, + 54511E8E209805F8005BD28F /* hashing_test.cc in Sources */, 5492E059202154AB00B64F25 /* FIRQuerySnapshotTests.mm in Sources */, 5492E050202154AA00B64F25 /* FIRCollectionReferenceTests.mm in Sources */, ABA495BB202B7E80008A7851 /* snapshot_version_test.cc in Sources */, @@ -1493,6 +1527,7 @@ AB38D93020236E21000A432D /* database_info_test.cc in Sources */, 5492E052202154AB00B64F25 /* FIRGeoPointTests.mm in Sources */, 5492E0C72021557E00B64F25 /* FSTSerializerBetaTests.mm in Sources */, + B6FB4690208F9BB300554BA2 /* executor_test.cc in Sources */, 5492E03520213FFC00B64F25 /* FSTSpecTests.mm in Sources */, 5492E057202154AB00B64F25 /* FIRSnapshotMetadataTests.mm in Sources */, 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */, @@ -1605,7 +1640,7 @@ /* Begin XCBuildConfiguration section */ 54C9EDF82040E16300A969CD /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9837221D251B8D40E7D7B454 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */; + baseConfigurationReference = 635C1D9B5E36BC4C12A35E70 /* Pods-Firestore_SwiftTests_iOS.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; @@ -1644,7 +1679,7 @@ }; 54C9EDF92040E16300A969CD /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 80025E2E892B94823962D11D /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */; + baseConfigurationReference = 39F1102E452A53A1F93AAA1F /* Pods-Firestore_SwiftTests_iOS.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; @@ -1891,10 +1926,6 @@ OTHER_LDFLAGS = ( "$(inherited)", "-l\"c++\"", - "-framework", - "\"OCMock\"", - "-framework", - "\"leveldb\"", ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1932,10 +1963,6 @@ OTHER_LDFLAGS = ( "$(inherited)", "-l\"c++\"", - "-framework", - "\"OCMock\"", - "-framework", - "\"leveldb\"", ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Firestore/Example/Podfile b/Firestore/Example/Podfile index 1de1906..c1e02c8 100644 --- a/Firestore/Example/Podfile +++ b/Firestore/Example/Podfile @@ -17,25 +17,56 @@ pod 'FirebaseFirestore', :path => '../../' target 'Firestore_Example_iOS' do platform :ios, '8.0' - target 'Firestore_Tests_iOS' do - inherit! :search_paths + # Test targets are below to avoid problems with duplicate symbols when + # building as a static library (see comments below). +end - pod 'leveldb-library' - pod 'OCMock' - pod 'GoogleTest', :podspec => 'Tests/GoogleTest/GoogleTest.podspec' - end +target 'Firestore_Tests_iOS' do + platform :ios, '8.0' - target 'Firestore_IntegrationTests_iOS' do - inherit! :search_paths + pod 'leveldb-library' + pod 'OCMock' + pod 'GoogleTest', :podspec => 'Tests/GoogleTest/GoogleTest.podspec' +end - pod 'OCMock' - end +target 'Firestore_IntegrationTests_iOS' do + platform :ios, '8.0' +end - target 'Firestore_SwiftTests_iOS' do - pod 'FirebaseFirestoreSwift', :path => '../../' - end +target 'Firestore_SwiftTests_iOS' do + platform :ios, '8.0' + pod 'FirebaseFirestoreSwift', :path => '../../' end target 'SwiftBuildTest' do platform :ios, '8.0' end + +# Firestore includes both Objective-C and C++ code, and the Firestore tests +# consist of both XCTest-based tests in Objective-C and GoogleTest-based tests +# in C++. The C++ tests must resolve the classes under test at link time, so +# CocoaPods usual strategy linking Frameworks to the app and then resolving +# those classes through run-time loading does not work in all cases. +# +# If use_frameworks! is disabled above, the project will encounter a ton of +# duplicate Objective-C class warnings during test runs. Some of the tests will +# fail too because duplicate classes also get duplicate static data and this +# violates the expectations of code we depend upon. +# +# The workaround is to strip duplicate dependencies out of the example app, +# which does not need them since it doesn't do anything other than act as a +# host to the tests. This is based on the workaround posted here: +# +# https://github.com/CocoaPods/CocoaPods/issues/7155 +# +# TODO(wilhuff): Reevaluate if this is needed once we require CocoaPods 1.5.1 +# which may address this. +pre_install do |installer| + test_target = installer.aggregate_targets.find do |target| + target.name == 'Pods-Firestore_Tests_iOS' + end + app_target = installer.aggregate_targets.find do |target| + target.name == 'Pods-Firestore_Example_iOS' + end + app_target.pod_targets = app_target.pod_targets - test_target.pod_targets +end diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm index 0454152..7ae9704 100644 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm @@ -27,6 +27,8 @@ #import "Firestore/Example/Tests/Util/FSTHelpers.h" +using firebase::firestore::model::DocumentKeySet; + NS_ASSUME_NONNULL_BEGIN @interface FSTQueryListenerTests : XCTestCase @@ -53,7 +55,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil); @@ -107,7 +109,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); FSTTargetChange *ackTarget = @@ -136,7 +138,7 @@ NS_ASSUME_NONNULL_BEGIN [listener mute]; }]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil); @@ -178,7 +180,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *fullListener = [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTTargetChange *ackTarget = @@ -218,7 +220,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *fullListener = [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); @@ -265,7 +267,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *fullListener = [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); @@ -298,7 +300,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *filteredListener = [self listenToQuery:query accumulatingSnapshots:filteredAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil); @@ -331,7 +333,7 @@ NS_ASSUME_NONNULL_BEGIN waitForSyncWhenOnline:YES] accumulatingSnapshots:events]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); FSTViewSnapshot *snap3 = @@ -374,7 +376,7 @@ NS_ASSUME_NONNULL_BEGIN waitForSyncWhenOnline:YES] accumulatingSnapshots:events]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); @@ -415,7 +417,7 @@ NS_ASSUME_NONNULL_BEGIN options:[FSTListenOptions defaultOptions] accumulatingSnapshots:events]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); [listener applyChangedOnlineState:FSTOnlineStateOnline]; // no event @@ -441,7 +443,7 @@ NS_ASSUME_NONNULL_BEGIN options:[FSTListenOptions defaultOptions] accumulatingSnapshots:events]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); [listener applyChangedOnlineState:FSTOnlineStateOffline]; // no event diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm index 63ce711..ec62d82 100644 --- a/Firestore/Example/Tests/Core/FSTViewTests.mm +++ b/Firestore/Example/Tests/Core/FSTViewTests.mm @@ -34,6 +34,7 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::ResourcePath; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -49,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testAddsDocumentsBasedOnQuery { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); @@ -77,7 +78,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testRemovesDocuments { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); @@ -108,7 +109,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testReturnsNilIfThereAreNoChanges { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); @@ -123,7 +124,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testDoesNotReturnNilForFirstChanges { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil); XCTAssertNotNil(snapshot); @@ -137,7 +138,7 @@ NS_ASSUME_NONNULL_BEGIN value:[FSTDoubleValue doubleValue:2]]; query = [query queryByAddingFilter:filter]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; 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); @@ -169,7 +170,7 @@ NS_ASSUME_NONNULL_BEGIN value:[FSTDoubleValue doubleValue:2]]; query = [query queryByAddingFilter:filter]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; 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); @@ -205,7 +206,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testRemovesDocumentsForQueryWithLimit { FSTQuery *query = [self queryForMessages]; query = [query queryBySettingLimit:2]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); @@ -239,7 +240,7 @@ NS_ASSUME_NONNULL_BEGIN query = [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:testutil::Field("num") ascending:YES]]; query = [query queryBySettingLimit:2]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{ @"num" : @1 }, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{ @"num" : @2 }, NO); @@ -283,7 +284,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testKeepsTrackOfLimboDocuments { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, NO); @@ -339,8 +340,8 @@ NS_ASSUME_NONNULL_BEGIN // 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 ])]; + FSTView *view = + [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{doc1.key, doc2.key}]; FSTTargetChange *markCurrent = [FSTTargetChange changeWithDocuments:@[] @@ -361,7 +362,7 @@ NS_ASSUME_NONNULL_BEGIN 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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -394,7 +395,7 @@ NS_ASSUME_NONNULL_BEGIN 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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -430,7 +431,7 @@ NS_ASSUME_NONNULL_BEGIN 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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -460,7 +461,7 @@ NS_ASSUME_NONNULL_BEGIN 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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -483,7 +484,7 @@ NS_ASSUME_NONNULL_BEGIN 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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -506,7 +507,7 @@ NS_ASSUME_NONNULL_BEGIN 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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; @@ -528,7 +529,7 @@ NS_ASSUME_NONNULL_BEGIN 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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -551,69 +552,69 @@ NS_ASSUME_NONNULL_BEGIN 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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); + XCTAssertEqual(changes.mutatedKeys, DocumentKeySet{}); FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/2", 0, @{}, YES); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc3.key ])); + XCTAssertEqual(changes.mutatedKeys, DocumentKeySet{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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key})); FSTDocument *doc2Prime = FSTTestDoc("rooms/eros/messages/1", 0, @{}, NO); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2Prime ])]; [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); + XCTAssertEqual(changes.mutatedKeys, DocumentKeySet{}); } - (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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{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 ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{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]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key})); FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/2", 0, @{}, NO); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ]) previousChanges:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key})); } @end diff --git a/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm b/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm index 2188b8a..e444d7a 100644 --- a/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm @@ -216,32 +216,29 @@ FIRTimestamp *TimestampWithMicros(int64_t seconds, int32_t micros) { } - (void)testTimestampsCanBePassedToQueriesInWhereClause { - FIRTimestamp *currentTimestamp = [FIRTimestamp timestamp]; - int64_t seconds = currentTimestamp.seconds; - int32_t micros = currentTimestamp.nanoseconds / 1000; FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ @"a" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros + 2), + @"timestamp" : TimestampWithMicros(100, 7), }, @"b" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros - 1), + @"timestamp" : TimestampWithMicros(100, 4), }, @"c" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros + 3), + @"timestamp" : TimestampWithMicros(100, 8), }, @"d" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros), + @"timestamp" : TimestampWithMicros(100, 5), }, @"e" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros + 1), + @"timestamp" : TimestampWithMicros(100, 6), } }]; - FIRQuerySnapshot *querySnapshot = [self - readDocumentSetForRef:[[testCollection queryWhereField:@"timestamp" - isGreaterThanOrEqualTo:TimestampWithMicros(seconds, micros)] - queryWhereField:@"timestamp" - isLessThan:TimestampWithMicros(seconds, micros + 3)]]; + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[[testCollection queryWhereField:@"timestamp" + isGreaterThanOrEqualTo:TimestampWithMicros(100, 5)] + queryWhereField:@"timestamp" + isLessThan:TimestampWithMicros(100, 8)]]; XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(querySnapshot), (@[ @"d", @"e", @"a" ])); } diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm index 9b6febe..f8091c0 100644 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm @@ -256,6 +256,155 @@ XCTAssertEqualObjects(document.data, finalData); } +- (void)testCannotSpecifyFieldMaskForMissingField { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTAssertThrowsSpecific( + { [doc setData:@{} mergeFields:@[ @"foo" ]]; }, NSException, + @"Field 'foo' is specified in your field mask but missing from your input data."); +} + +- (void)testCanSetASubsetOfFieldsUsingMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{@"desc" : @"Description", @"owner" : @"Sebastian"}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : @"NewDescription", @"owner" : @"Sebastian"} + mergeFields:@[ @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testDoesNotApplyFieldDeleteOutsideOfMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{@"desc" : @"Description", @"owner" : @"Sebastian"}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : [FIRFieldValue fieldValueForDelete], @"owner" : @"Sebastian"} + mergeFields:@[ @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testDoesNotApplyFieldTransformOutsideOfMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{@"desc" : @"Description", @"owner" : @"Sebastian"}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : [FIRFieldValue fieldValueForServerTimestamp], @"owner" : @"Sebastian"} + mergeFields:@[ @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testCanSetEmptyFieldMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = initialData; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : [FIRFieldValue fieldValueForServerTimestamp], @"owner" : @"Sebastian"} + mergeFields:@[] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testCanSpecifyFieldsMultipleTimesInFieldMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{ + @"desc" : @"Description", + @"owner" : @{@"name" : @"Sebastian", @"email" : @"new@xyz.com"} + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{ + @"desc" : @"NewDescription", + @"owner" : @{@"name" : @"Sebastian", @"email" : @"new@xyz.com"} + } + mergeFields:@[ @"owner.name", @"owner", @"owner" ] + 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 }]; diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index ad911ce..920e3c5 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -25,7 +25,6 @@ #import "Firestore/Source/API/FSTUserDataConverter.h" #import "Firestore/Source/Core/FSTFirestoreClient.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocumentKey.h" #import "Firestore/Source/Model/FSTFieldValue.h" diff --git a/Firestore/Example/Tests/Integration/FSTStreamTests.mm b/Firestore/Example/Tests/Integration/FSTStreamTests.mm index 7e37913..2e5c9b6 100644 --- a/Firestore/Example/Tests/Integration/FSTStreamTests.mm +++ b/Firestore/Example/Tests/Integration/FSTStreamTests.mm @@ -29,12 +29,14 @@ #include "Firestore/core/src/firebase/firestore/auth/empty_credentials_provider.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; using firebase::firestore::auth::EmptyCredentialsProvider; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::SnapshotVersion; /** Exposes otherwise private methods for testing. */ @interface FSTStream (Testing) @@ -101,13 +103,13 @@ using firebase::firestore::model::DatabaseId; } - (void)watchStreamDidChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + snapshotVersion:(const SnapshotVersion &)snapshotVersion { [_states addObject:@"watchStreamDidChange"]; [_expectation fulfill]; _expectation = nil; } -- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)writeStreamDidReceiveResponseWithVersion:(const SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results { [_states addObject:@"writeStreamDidReceiveResponseWithVersion"]; [_expectation fulfill]; diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm index 362f46f..0e160c0 100644 --- a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm @@ -29,7 +29,6 @@ #import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" #import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentKey.h" @@ -50,6 +49,7 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::FieldMask; using firebase::firestore::model::Precondition; +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -125,7 +125,7 @@ NS_ASSUME_NONNULL_BEGIN XCTAssertEqual(decoded.batchID, model.batchID); XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime); XCTAssertEqualObjects(decoded.mutations, model.mutations); - XCTAssertEqualObjects([decoded keys], [model keys]); + XCTAssertEqual([decoded keys], [model keys]); } - (void)testEncodesDocumentAsMaybeDocument { @@ -162,7 +162,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testEncodesQueryData { FSTQuery *query = FSTTestQuery("room"); FSTTargetID targetID = 42; - FSTSnapshotVersion *version = FSTTestVersion(1039); + SnapshotVersion version = testutil::Version(1039); NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1039); FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index 3565e2e..e10fb12 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -41,19 +41,15 @@ #import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +namespace testutil = firebase::firestore::testutil; using firebase::firestore::auth::User; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; 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; @@ -140,7 +136,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, 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); + SnapshotVersion version = testutil::Version(documentVersion); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch @@ -227,8 +223,8 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1 localWriteTime:[FIRTimestamp timestamp] mutations:@[ set1, set2 ]]; - FSTDocumentKeySet *keys = [batch keys]; - XCTAssertEqual(keys.count, 2); + DocumentKeySet keys = [batch keys]; + XCTAssertEqual(keys.size(), 2u); } - (void)testHandlesSetMutation { @@ -805,6 +801,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTQuery *query = FSTTestQuery("foo/bar"); FSTQueryData *queryData = [self.localStore allocateQuery:query]; + FSTListenSequenceNumber initialSequenceNumber = queryData.sequenceNumber; FSTBoxedTargetID *targetID = @(queryData.targetID); NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); @@ -818,7 +815,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingResponses = [NSMutableDictionary dictionary]; FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(1000) + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(1000) listenTargets:listens pendingTargetResponses:pendingResponses]; [aggregator addWatchChanges:@[ watchChange ]]; @@ -831,6 +828,10 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, // Should come back with the same resume token FSTQueryData *queryData2 = [self.localStore allocateQuery:query]; XCTAssertEqualObjects(queryData2.resumeToken, resumeToken); + + // The sequence number should have been bumped when we saved the new resume token. + FSTListenSequenceNumber newSequenceNumber = queryData2.sequenceNumber; + XCTAssertGreaterThan(newSequenceNumber, initialSequenceNumber); } - (void)testRemoteDocumentKeysForTarget { @@ -848,13 +849,14 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; - FSTDocumentKeySet *keys = [self.localStore remoteDocumentKeysForTarget:2]; - FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); + DocumentKeySet keys = [self.localStore remoteDocumentKeysForTarget:2]; + DocumentKeySet expected{testutil::Key("foo/bar"), testutil::Key("foo/baz")}; + XCTAssertEqual(keys, expected); [self restartWithNoopGarbageCollector]; keys = [self.localStore remoteDocumentKeysForTarget:2]; - FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); + XCTAssertEqual(keys, (DocumentKeySet{testutil::Key("foo/bar"), testutil::Key("foo/baz")})); } @end diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm b/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm index 429a83a..44b49de 100644 --- a/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm @@ -19,7 +19,6 @@ #include <set> #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTEagerGarbageCollector.h" #import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Local/FSTQueryData.h" @@ -32,6 +31,8 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -130,9 +131,9 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; XCTAssertNotEqualObjects(queryData2.resumeToken, queryData1.resumeToken); - XCTAssertNotEqualObjects(queryData2.snapshotVersion, queryData1.snapshotVersion); + XCTAssertNotEqual(queryData2.snapshotVersion, queryData1.snapshotVersion); XCTAssertEqualObjects(result.resumeToken, queryData2.resumeToken); - XCTAssertEqualObjects(result.snapshotVersion, queryData2.snapshotVersion); + XCTAssertEqual(result.snapshotVersion, queryData2.snapshotVersion); }); } @@ -278,12 +279,12 @@ NS_ASSUME_NONNULL_BEGIN [self addMatchingKey:key2 forTargetID:1]; [self addMatchingKey:key3 forTargetID:2]; - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], @[ key3 ]); + XCTAssertEqual([self.queryCache matchingKeysForTargetID:1], (DocumentKeySet{key1, key2})); + XCTAssertEqual([self.queryCache matchingKeysForTargetID:2], (DocumentKeySet{key3})); [self addMatchingKey:key1 forTargetID:2]; - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], (@[ key1, key3 ])); + XCTAssertEqual([self.queryCache matchingKeysForTargetID:1], (DocumentKeySet{key1, key2})); + XCTAssertEqual([self.queryCache matchingKeysForTargetID:2], (DocumentKeySet{key1, key3})); }); } @@ -386,19 +387,18 @@ NS_ASSUME_NONNULL_BEGIN if ([self isTestBaseClass]) return; self.persistence.run("testLastRemoteSnapshotVersion", [&]() { - XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], - [FSTSnapshotVersion noVersion]); + XCTAssertEqual([self.queryCache lastRemoteSnapshotVersion], SnapshotVersion::None()); // Can set the snapshot version. - [self.queryCache setLastRemoteSnapshotVersion:FSTTestVersion(42)]; - XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); + [self.queryCache setLastRemoteSnapshotVersion:testutil::Version(42)]; + XCTAssertEqual([self.queryCache lastRemoteSnapshotVersion], testutil::Version(42)); }); // Snapshot version persists restarts. self.queryCache = [self.persistence queryCache]; self.persistence.run("testLastRemoteSnapshotVersion restart", [&]() { [self.queryCache start]; - XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); + XCTAssertEqual([self.queryCache lastRemoteSnapshotVersion], testutil::Version(42)); }); } @@ -424,19 +424,17 @@ NS_ASSUME_NONNULL_BEGIN targetID:targetID listenSequenceNumber:sequenceNumber purpose:FSTQueryPurposeListen - snapshotVersion:FSTTestVersion(version) + snapshotVersion:testutil::Version(version) resumeToken:resumeToken]; } - (void)addMatchingKey:(const DocumentKey &)key forTargetID:(FSTTargetID)targetID { - FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; - keys = [keys setByAddingObject:key]; + DocumentKeySet keys{key}; [self.queryCache addMatchingKeys:keys forTargetID:targetID]; } - (void)removeMatchingKey:(const DocumentKey &)key forTargetID:(FSTTargetID)targetID { - FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; - keys = [keys setByAddingObject:key]; + DocumentKeySet keys{key}; [self.queryCache removeMatchingKeys:keys forTargetID:targetID]; } diff --git a/Firestore/Example/Tests/Model/FSTDocumentTests.mm b/Firestore/Example/Tests/Model/FSTDocumentTests.mm index 24858c5..4e3517c 100644 --- a/Firestore/Example/Tests/Model/FSTDocumentTests.mm +++ b/Firestore/Example/Tests/Model/FSTDocumentTests.mm @@ -18,7 +18,6 @@ #import <XCTest/XCTest.h> -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" @@ -28,6 +27,7 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -38,20 +38,20 @@ NS_ASSUME_NONNULL_BEGIN - (void)testConstructor { DocumentKey key = testutil::Key("messages/first"); - FSTSnapshotVersion *version = FSTTestVersion(1); + SnapshotVersion version = testutil::Version(1); FSTObjectValue *data = FSTTestObjectValue(@{ @"a" : @1 }); FSTDocument *doc = [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; XCTAssertEqualObjects(doc.key, FSTTestDocKey(@"messages/first")); - XCTAssertEqualObjects(doc.version, version); + XCTAssertEqual(doc.version, version); XCTAssertEqualObjects(doc.data, data); XCTAssertEqual(doc.hasLocalMutations, NO); } - (void)testExtractsFields { DocumentKey key = testutil::Key("rooms/eros"); - FSTSnapshotVersion *version = FSTTestVersion(1); + SnapshotVersion version = testutil::Version(1); FSTObjectValue *data = FSTTestObjectValue(@{ @"desc" : @"Discuss all the project related stuff", @"owner" : @{@"name" : @"Jonny", @"title" : @"scallywag"} diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.mm b/Firestore/Example/Tests/Model/FSTMutationTests.mm index 936bd38..b9f98ce 100644 --- a/Firestore/Example/Tests/Model/FSTMutationTests.mm +++ b/Firestore/Example/Tests/Model/FSTMutationTests.mm @@ -135,7 +135,7 @@ using firebase::firestore::model::TransformOperation; FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData key:FSTTestDocKey(@"collection/key") - version:FSTTestVersion(0) + version:testutil::Version(0) hasLocalMutations:YES]; XCTAssertEqualObjects(transformedDoc, expectedDoc); @@ -301,7 +301,7 @@ using firebase::firestore::model::TransformOperation; FSTDocument *expectedDoc = [FSTDocument documentWithData:FSTTestObjectValue(expectedData) key:FSTTestDocKey(@"collection/key") - version:FSTTestVersion(0) + version:testutil::Version(0) hasLocalMutations:YES]; XCTAssertEqualObjects(transformedDoc, expectedDoc); @@ -315,7 +315,7 @@ using firebase::firestore::model::TransformOperation; @"collection/key", @{@"foo.bar" : [FIRFieldValue fieldValueForServerTimestamp]}); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] - initWithVersion:FSTTestVersion(1) + initWithVersion:testutil::Version(1) transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]]; FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc @@ -340,7 +340,7 @@ using firebase::firestore::model::TransformOperation; // Server just sends null transform results for array operations. FSTMutationResult *mutationResult = [[FSTMutationResult alloc] - initWithVersion:FSTTestVersion(1) + initWithVersion:testutil::Version(1) transformResults:@[ [FSTNullValue nullValue], [FSTNullValue nullValue] ]]; FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc @@ -368,7 +368,7 @@ using firebase::firestore::model::TransformOperation; FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"}); FSTMutationResult *mutationResult = - [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + [[FSTMutationResult alloc] initWithVersion:testutil::Version(4) transformResults:nil]; FSTMaybeDocument *setDoc = [set applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp @@ -384,7 +384,7 @@ using firebase::firestore::model::TransformOperation; FSTMutation *patch = FSTTestPatchMutation("collection/key", @{@"foo" : @"new-bar"}, {}); FSTMutationResult *mutationResult = - [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + [[FSTMutationResult alloc] initWithVersion:testutil::Version(4) transformResults:nil]; FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp @@ -394,15 +394,15 @@ using firebase::firestore::model::TransformOperation; 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 \ - baseDocument:base \ - localWriteTime:_timestamp \ - mutationResult:mutationResult]; \ - XCTAssertEqualObjects(actual, expected); \ +#define ASSERT_VERSION_TRANSITION(mutation, base, expected) \ + do { \ + FSTMutationResult *mutationResult = \ + [[FSTMutationResult alloc] initWithVersion:testutil::Version(0) transformResults:nil]; \ + FSTMaybeDocument *actual = [mutation applyTo:base \ + baseDocument:base \ + localWriteTime:_timestamp \ + mutationResult:mutationResult]; \ + XCTAssertEqualObjects(actual, expected); \ } while (0); /** diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm index 6ac2a6b..84d0fa1 100644 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -18,6 +18,7 @@ #import <XCTest/XCTest.h> +#import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentKey.h" @@ -28,7 +29,11 @@ #import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" + +namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -55,7 +60,7 @@ NS_ASSUME_NONNULL_BEGIN listens[targetID] = dummyQueryData; } FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(3) + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3) listenTargets:listens pendingTargetResponses:outstanding]; [aggregator addWatchChanges:watchChanges]; @@ -81,7 +86,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); @@ -143,7 +148,7 @@ NS_ASSUME_NONNULL_BEGIN outstanding:pendingResponses changes:@[ change1, change2, change3, change4 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(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.size(), 1); @@ -171,7 +176,7 @@ NS_ASSUME_NONNULL_BEGIN [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); // doc1 is ignored because it was part of an inactive target XCTAssertEqual(event.documentUpdates.size(), 0); @@ -215,7 +220,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2, change3, change4, change5 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 3); XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); @@ -239,7 +244,7 @@ NS_ASSUME_NONNULL_BEGIN [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.count, 1); @@ -267,7 +272,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 1); XCTAssertEqualObjects(event.documentUpdates.at(doc1b.key), doc1b); @@ -291,7 +296,7 @@ NS_ASSUME_NONNULL_BEGIN [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.count, 1); FSTTargetChange *targetChange = event.targetChanges[@1]; @@ -333,7 +338,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2, change3, change4, change5, change6 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); @@ -366,7 +371,7 @@ NS_ASSUME_NONNULL_BEGIN [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.count, 1); XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]); @@ -388,7 +393,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2, change3 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.count, 0); XCTAssertEqual(aggregator.existenceFilters.count, 2); @@ -420,7 +425,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2, change3 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); @@ -430,7 +435,7 @@ NS_ASSUME_NONNULL_BEGIN FSTUpdateMapping *mapping1 = [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); @@ -439,7 +444,7 @@ NS_ASSUME_NONNULL_BEGIN // Mapping is reset XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]); // Reset the resume snapshot - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(0)); + XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(0)); // Target needs to be set to not current XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent); XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0); @@ -448,7 +453,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testDocumentUpdate { FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTDeletedDocument *deletedDoc1 = - [FSTDeletedDocument documentWithKey:doc1.key version:FSTTestVersion(3)]; + [FSTDeletedDocument documentWithKey:doc1.key version:testutil::Version(3)]; FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO); @@ -467,7 +472,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); @@ -476,7 +481,7 @@ NS_ASSUME_NONNULL_BEGIN [event addDocumentUpdate:deletedDoc1]; [event addDocumentUpdate:doc3]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 3); // doc1 is replaced XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), deletedDoc1); @@ -511,12 +516,12 @@ NS_ASSUME_NONNULL_BEGIN FSTUpdateMapping *mapping1 = [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]]; XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(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].snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2); } @@ -544,12 +549,12 @@ NS_ASSUME_NONNULL_BEGIN FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]]; XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(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].snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone); XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3); } @@ -557,20 +562,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)testSynthesizeDeletes { FSTWatchChange *shouldSynthesize = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1 ]]; - FSTWatchChange *wrongState = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @2 ]]; - FSTWatchChange *hasDocument = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @3 ]]; - FSTDocument *doc = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); - FSTWatchChange *docChange = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @3 ] - removedTargetIDs:@[] - documentKey:doc.key - document:doc]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1, @2, @3 ] - outstanding:_noPendingResponses - changes:@[ shouldSynthesize, wrongState, hasDocument, docChange ]]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ shouldSynthesize ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; DocumentKey synthesized = DocumentKey::FromPathString("docs/2"); @@ -581,14 +576,41 @@ NS_ASSUME_NONNULL_BEGIN FSTDeletedDocument *expected = [FSTDeletedDocument documentWithKey:synthesized version:event.snapshotVersion]; XCTAssertEqualObjects(expected, event.documentUpdates.at(synthesized)); + XCTAssertTrue(event.limboDocumentChanges.contains(synthesized)); +} + +- (void)testDoesntSynthesizeDeletesForWrongState { + FSTWatchChange *wrongState = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @2 ]]; - DocumentKey notSynthesized1 = DocumentKey::FromPathString("docs/no1"); - [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@2] key:notSynthesized1]; - XCTAssertEqual(event.documentUpdates.find(notSynthesized1), event.documentUpdates.end()); + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @2 ] outstanding:_noPendingResponses changes:@[ wrongState ]]; + FSTRemoteEvent *event = [aggregator remoteEvent]; + + DocumentKey notSynthesized = DocumentKey::FromPathString("docs/no1"); + [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@2] key:notSynthesized]; + XCTAssertEqual(event.documentUpdates.find(notSynthesized), event.documentUpdates.end()); + XCTAssertFalse(event.limboDocumentChanges.contains(notSynthesized)); +} + +- (void)testDoesntSynthesizeDeletesForExistingDoc { + FSTWatchChange *hasDocument = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @3 ]]; + FSTDocument *doc = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); + FSTWatchChange *docChange = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @3 ] + removedTargetIDs:@[] + documentKey:doc.key + document:doc]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @3 ] + outstanding:_noPendingResponses + changes:@[ hasDocument, docChange ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@3] key:doc.key]; FSTMaybeDocument *docData = event.documentUpdates.at(doc.key); XCTAssertFalse([docData isKindOfClass:[FSTDeletedDocument class]]); + XCTAssertFalse(event.limboDocumentChanges.contains(doc.key)); } - (void)testFilterUpdates { @@ -599,44 +621,108 @@ NS_ASSUME_NONNULL_BEGIN documentKey:newDoc.key document:newDoc]; - FSTWatchTargetChange *resetTargetChange = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @2 ] - resumeToken:_resumeToken1]; - FSTWatchChange *existingDocChange = - [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2 ] + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:existingDoc.key document:existingDoc]; FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1, @2 ] + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses - changes:@[ newDocChange, resetTargetChange, existingDocChange ]]; + changes:@[ newDocChange, existingDocChange ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - FSTDocumentKeySet *existingKeys = [[FSTDocumentKeySet keySet] setByAddingObject:existingDoc.key]; + DocumentKeySet existingKeys = DocumentKeySet{existingDoc.key}; FSTTargetChange *updateChange = event.targetChanges[@1]; XCTAssertTrue([updateChange.mapping isKindOfClass:[FSTUpdateMapping class]]); FSTUpdateMapping *update = (FSTUpdateMapping *)updateChange.mapping; FSTDocumentKey *existingDocKey = existingDoc.key; FSTDocumentKey *newDocKey = newDoc.key; - XCTAssertTrue([update.addedDocuments containsObject:existingDocKey]); + XCTAssertTrue(update.addedDocuments.contains(existingDocKey)); - [event filterUpdatesFromTargetChange:updateChange existingDocuments:existingKeys]; + [update filterUpdatesUsingExistingKeys:existingKeys]; // Now it's been filtered, since it already existed. - XCTAssertFalse([update.addedDocuments containsObject:existingDocKey]); - XCTAssertTrue([update.addedDocuments containsObject:newDocKey]); + XCTAssertFalse(update.addedDocuments.contains(existingDocKey)); + XCTAssertTrue(update.addedDocuments.contains(newDocKey)); +} + +- (void)testDoesntFilterResets { + FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO); + const DocumentKey &existingDocKey = existingDoc.key; + FSTWatchTargetChange *resetTargetChange = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @2 ] + resumeToken:_resumeToken1]; + FSTWatchChange *existingDocChange = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[] + documentKey:existingDocKey + document:existingDoc]; + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @2 ] + outstanding:_noPendingResponses + changes:@[ resetTargetChange, existingDocChange ]]; + FSTRemoteEvent *event = [aggregator remoteEvent]; + DocumentKeySet existingKeys = DocumentKeySet{existingDocKey}; FSTTargetChange *resetChange = event.targetChanges[@2]; XCTAssertTrue([resetChange.mapping isKindOfClass:[FSTResetMapping class]]); FSTResetMapping *resetMapping = (FSTResetMapping *)resetChange.mapping; - XCTAssertTrue([resetMapping.documents containsObject:existingDocKey]); + XCTAssertTrue(resetMapping.documents.contains(existingDocKey)); - [event filterUpdatesFromTargetChange:resetChange existingDocuments:existingKeys]; + [resetMapping filterUpdatesUsingExistingKeys:existingKeys]; // Document is still there, even though it already exists. Reset mappings don't get filtered. - XCTAssertTrue([resetMapping.documents containsObject:existingDocKey]); + XCTAssertTrue(resetMapping.documents.contains(existingDocKey)); +} + +- (void)testTracksLimboDocuments { + // Add 3 docs: 1 is limbo and non-limbo, 2 is limbo-only, 3 is non-limbo + FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"key" : @"value"}, NO); + FSTDocument *doc2 = FSTTestDoc("docs/2", 1, @{@"key" : @"value"}, NO); + FSTDocument *doc3 = FSTTestDoc("docs/3", 1, @{@"key" : @"value"}, NO); + + // Target 2 is a limbo target + + FSTWatchChange *docChange1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *docChange2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChange *docChange3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc3.key + document:doc3]; + + FSTWatchChange *targetsChange = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1, @2 ]]; + + NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary]; + listens[@1] = [FSTQueryData alloc]; + listens[@2] = [[FSTQueryData alloc] initWithQuery:[FSTQuery alloc] + targetID:2 + listenSequenceNumber:1000 + purpose:FSTQueryPurposeLimboResolution]; + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3) + listenTargets:listens + pendingTargetResponses:@{}]; + + [aggregator addWatchChanges:@[ docChange1, docChange2, docChange3, targetsChange ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + DocumentKeySet limboDocChanges = event.limboDocumentChanges; + // Doc1 is in both limbo and non-limbo targets, therefore not tracked as limbo + XCTAssertFalse(limboDocChanges.contains(doc1.key)); + // Doc2 is only in the limbo target, so is tracked as a limbo document + XCTAssertTrue(limboDocChanges.contains(doc2.key)); + // Doc3 is only in the non-limbo target, therefore not tracked as limbo + XCTAssertFalse(limboDocChanges.contains(doc3.key)); } @end diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm index bbb3822..da47aaa 100644 --- a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm @@ -37,7 +37,6 @@ #import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" #import "Firestore/Source/API/FIRFieldValue+Internal.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentKey.h" @@ -58,10 +57,12 @@ namespace testutil = firebase::firestore::testutil; namespace util = firebase::firestore::util; +using firebase::Timestamp; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldTransform; using firebase::firestore::model::Precondition; +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -424,8 +425,7 @@ NS_ASSUME_NONNULL_BEGIN precondition:Precondition::UpdateTime(testutil::Version(4))]; GCFSWrite *proto = [GCFSWrite message]; proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; - proto.currentDocument.updateTime = - [self.serializer encodedTimestamp:[[FIRTimestamp alloc] initWithSeconds:0 nanoseconds:4000]]; + proto.currentDocument.updateTime = [self.serializer encodedTimestamp:Timestamp{0, 4000}]; [self assertRoundTripForMutation:mutation proto:proto]; } @@ -708,7 +708,7 @@ NS_ASSUME_NONNULL_BEGIN targetID:1 listenSequenceNumber:0 purpose:FSTQueryPurposeListen - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() resumeToken:FSTTestData(1, 2, 3, -1)]; GCFSTarget *expected = [GCFSTarget message]; @@ -729,7 +729,7 @@ NS_ASSUME_NONNULL_BEGIN targetID:1 listenSequenceNumber:0 purpose:FSTQueryPurposeListen - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() resumeToken:[NSData data]]; } diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h index e1ea2fb..6951f9c 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h @@ -18,6 +18,8 @@ #import "Firestore/Source/Remote/FSTDatastore.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + NS_ASSUME_NONNULL_BEGIN @interface FSTMockDatastore : FSTDatastore @@ -41,11 +43,13 @@ NS_ASSUME_NONNULL_BEGIN /** Injects an Added WatchChange that marks the given targetIDs current. */ - (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion: + (const firebase::firestore::model::SnapshotVersion &)snapshotVersion resumeToken:(NSData *)resumeToken; /** Injects a WatchChange as though it had come from the backend. */ -- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap; +- (void)writeWatchChange:(FSTWatchChange *)change + snapshotVersion:(const firebase::firestore::model::SnapshotVersion &)snap; /** Injects a stream failure as though it had come from the backend. */ - (void)failWatchStreamWithError:(NSError *)error; @@ -67,7 +71,7 @@ NS_ASSUME_NONNULL_BEGIN - (int)writesSent; /** Injects a write ack as though it had come from the backend in response to a write. */ -- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)ackWriteWithVersion:(const firebase::firestore::model::SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results; /** Injects a stream failure as though it had come from the backend. */ diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm index 6715b24..dd34556 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm @@ -16,7 +16,6 @@ #import "Firestore/Example/Tests/SpecTests/FSTMockDatastore.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" @@ -36,6 +35,7 @@ using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::EmptyCredentialsProvider; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::SnapshotVersion; @class GRPCProtoCall; @@ -120,9 +120,9 @@ NS_ASSUME_NONNULL_BEGIN FSTLog(@"watchQuery: %d: %@", query.targetID, query.query); self.datastore.watchStreamRequestCount += 1; // Snapshot version is ignored on the wire - FSTQueryData *sentQueryData = - [query queryDataByReplacingSnapshotVersion:[FSTSnapshotVersion noVersion] - resumeToken:query.resumeToken]; + FSTQueryData *sentQueryData = [query queryDataByReplacingSnapshotVersion:SnapshotVersion::None() + resumeToken:query.resumeToken + sequenceNumber:query.sequenceNumber]; self.activeTargets[@(query.targetID)] = sentQueryData; } @@ -138,7 +138,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Helper methods. -- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(const SnapshotVersion &)snap { if ([change isKindOfClass:[FSTWatchTargetChange class]]) { FSTWatchTargetChange *targetChange = (FSTWatchTargetChange *)change; if (targetChange.cause) { @@ -242,7 +242,7 @@ NS_ASSUME_NONNULL_BEGIN #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 +- (void)ackWriteWithVersion:(const SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results { [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; } @@ -326,7 +326,7 @@ NS_ASSUME_NONNULL_BEGIN return [self.writeStream sentMutationsCount]; } -- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)ackWriteWithVersion:(const SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results { [self.writeStream ackWriteWithVersion:commitVersion mutationResults:results]; } @@ -340,11 +340,11 @@ NS_ASSUME_NONNULL_BEGIN [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded targetIDs:targetIDs cause:nil]; - [self writeWatchChange:change snapshotVersion:[FSTSnapshotVersion noVersion]]; + [self writeWatchChange:change snapshotVersion:SnapshotVersion::None()]; } - (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion:(const SnapshotVersion &)snapshotVersion resumeToken:(NSData *)resumeToken { FSTWatchTargetChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent @@ -353,7 +353,7 @@ NS_ASSUME_NONNULL_BEGIN [self writeWatchChange:change snapshotVersion:snapshotVersion]; } -- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(const SnapshotVersion &)snap { [self.watchStream writeWatchChange:change snapshotVersion:snap]; } diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index 128f825..5a7cb72 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -24,7 +24,6 @@ #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTEagerGarbageCollector.h" #import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" #import "Firestore/Source/Local/FSTPersistence.h" @@ -46,11 +45,15 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +namespace testutil = firebase::firestore::testutil; namespace util = firebase::firestore::util; using firebase::firestore::auth::User; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; NS_ASSUME_NONNULL_BEGIN @@ -145,8 +148,8 @@ static NSString *const kNoIOSTag = @"no-ios"; } } -- (FSTSnapshotVersion *)parseVersion:(NSNumber *_Nullable)version { - return FSTTestVersion(version.longLongValue); +- (SnapshotVersion)parseVersion:(NSNumber *_Nullable)version { + return testutil::Version(version.longLongValue); } - (FSTDocumentViewChange *)parseChange:(NSArray *)change ofType:(FSTDocumentViewChangeType)type { @@ -254,9 +257,11 @@ static NSString *const kNoIOSTag = @"no-ios"; NSArray *docSpec = watchEntity[@"doc"]; FSTDocumentKey *key = FSTTestDocKey(docSpec[0]); FSTObjectValue *value = FSTTestObjectValue(docSpec[2]); - FSTSnapshotVersion *version = [self parseVersion:docSpec[1]]; - FSTMaybeDocument *doc = - [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; + SnapshotVersion version = [self parseVersion:docSpec[1]]; + FSTMaybeDocument *doc = [FSTDocument documentWithData:value + key:key + version:std::move(version) + hasLocalMutations:NO]; FSTWatchChange *change = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"] removedTargetIDs:watchEntity[@"removedTargets"] @@ -309,7 +314,7 @@ static NSString *const kNoIOSTag = @"no-ios"; } - (void)doWriteAck:(NSDictionary *)spec { - FSTSnapshotVersion *version = [self parseVersion:spec[@"version"]]; + SnapshotVersion version = [self parseVersion:spec[@"version"]]; NSNumber *expectUserCallback = spec[@"expectUserCallback"]; FSTMutationResult *mutationResult = @@ -550,7 +555,7 @@ static NSString *const kNoIOSTag = @"no-ios"; targetID:targetID listenSequenceNumber:0 purpose:FSTQueryPurposeListen - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() resumeToken:resumeToken]; }]; self.driver.expectedActiveTargets = expectedActiveTargets; @@ -598,10 +603,13 @@ static NSString *const kNoIOSTag = @"no-ios"; // 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); + XCTAssertNotNil(actual); + if (actual) { + XCTAssertEqualObjects(actual.query, queryData.query); + XCTAssertEqual(actual.targetID, queryData.targetID); + XCTAssertEqual(actual.snapshotVersion, queryData.snapshotVersion); + XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken); + } [actualTargets removeObjectForKey:targetID]; }]; diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index ac44cb5..cfd8974 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -25,13 +25,13 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTDocumentKey; @class FSTMutation; @class FSTMutationResult; @class FSTQuery; @class FSTQueryData; -@class FSTSnapshotVersion; @class FSTViewSnapshot; @class FSTWatchChange; @protocol FSTGarbageCollector; @@ -150,7 +150,7 @@ typedef std::unordered_map<firebase::firestore::auth::User, * simulating the server having sent a complete snapshot. */ - (void)receiveWatchChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot; + snapshotVersion:(const firebase::firestore::model::SnapshotVersion &)snapshot; /** * Delivers a watch stream error as if the Streaming Watch backend has generated some kind of error. @@ -195,7 +195,8 @@ typedef std::unordered_map<firebase::firestore::auth::User, * the mutation. Snapshot versions must be monotonically increasing. * @param mutationResults The mutation results for the write that is being acked. */ -- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion +- (FSTOutstandingWrite *)receiveWriteAckWithVersion: + (const firebase::firestore::model::SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)mutationResults; /** diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index f167ce5..2aa0e30 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -24,7 +24,6 @@ #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Core/FSTSyncEngine.h" #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Local/FSTPersistence.h" @@ -49,6 +48,7 @@ using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; NS_ASSUME_NONNULL_BEGIN @@ -243,7 +243,7 @@ NS_ASSUME_NONNULL_BEGIN }]; } -- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion +- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(const SnapshotVersion &)commitVersion mutationResults: (NSArray<FSTMutationResult *> *)mutationResults { FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; @@ -333,7 +333,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)receiveWatchChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot { + snapshotVersion:(const SnapshotVersion &)snapshot { [self.dispatchQueue dispatchSync:^{ [self.datastore writeWatchChange:change snapshotVersion:snapshot]; }]; diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index 131da2d..ccc01ca 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -21,7 +21,6 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" @@ -40,7 +39,6 @@ @class FSTQuery; @class FSTRemoteEvent; @class FSTSetMutation; -@class FSTSnapshotVersion; @class FSTSortOrder; @class FSTTargetChange; @class FIRTimestamp; @@ -182,15 +180,9 @@ 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(const absl::string_view path, FSTTestSnapshotVersion version, diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index 888a45f..dbd19aa 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -29,7 +29,6 @@ #import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Core/FSTView.h" #import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" @@ -45,6 +44,7 @@ #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/field_mask.h" #include "Firestore/core/src/firebase/firestore/model/field_transform.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" @@ -67,15 +67,13 @@ using firebase::firestore::model::Precondition; using firebase::firestore::model::ResourcePath; using firebase::firestore::model::ServerTimestampTransform; using firebase::firestore::model::TransformOperation; +using firebase::firestore::model::DocumentKeySet; 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; - FIRTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second) { NSDate *date = FSTTestDate(year, month, day, hour, minute, second); return [FIRTimestamp timestampWithDate:date]; @@ -150,22 +148,6 @@ 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; - - FIRTimestamp *timestamp = [[FIRTimestamp alloc] initWithSeconds:seconds nanoseconds:nanos]; - return [FSTSnapshotVersion versionWithTimestamp:timestamp]; -} - FSTDocument *FSTTestDoc(const absl::string_view path, FSTTestSnapshotVersion version, NSDictionary<NSString *, id> *data, @@ -173,14 +155,14 @@ FSTDocument *FSTTestDoc(const absl::string_view path, DocumentKey key = testutil::Key(path); return [FSTDocument documentWithData:FSTTestObjectValue(data) key:key - version:FSTTestVersion(version) + version:testutil::Version(version) hasLocalMutations:hasMutations]; } FSTDeletedDocument *FSTTestDeletedDoc(const absl::string_view path, FSTTestSnapshotVersion version) { DocumentKey key = testutil::Key(path); - return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)]; + return [FSTDeletedDocument documentWithKey:key version:testutil::Version(version)]; } FSTDocumentKeyReference *FSTTestRef(const absl::string_view projectID, @@ -355,17 +337,17 @@ NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion s FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, NSArray<NSString *> *addedKeys, NSArray<NSString *> *removedKeys) { - FSTDocumentKeySet *added = [FSTDocumentKeySet keySet]; + DocumentKeySet added; for (NSString *keyPath in addedKeys) { - FSTDocumentKey *key = FSTTestDocKey(keyPath); - added = [added setByAddingObject:key]; + added = added.insert(testutil::Key(util::MakeStringView(keyPath))); } - FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet]; + DocumentKeySet removed; for (NSString *keyPath in removedKeys) { - FSTDocumentKey *key = FSTTestDocKey(keyPath); - removed = [removed setByAddingObject:key]; + removed = removed.insert(testutil::Key(util::MakeStringView(keyPath))); } - return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed]; + return [FSTLocalViewChanges changesForQuery:query + addedKeys:std::move(added) + removedKeys:std::move(removed)]; } NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm index c2fc546..da67a5b 100644 --- a/Firestore/Source/API/FIRDocumentReference.mm +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -125,6 +125,11 @@ NS_ASSUME_NONNULL_BEGIN } - (void)setData:(NSDictionary<NSString *, id> *)documentData + mergeFields:(NSArray<id> *)mergeFields { + return [self setData:documentData mergeFields:mergeFields completion:nil]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData completion:(nullable void (^)(NSError *_Nullable error))completion { return [self setData:documentData merge:NO completion:completion]; } @@ -132,8 +137,19 @@ NS_ASSUME_NONNULL_BEGIN - (void)setData:(NSDictionary<NSString *, id> *)documentData merge:(BOOL)merge completion:(nullable void (^)(NSError *_Nullable error))completion { - FSTParsedSetData *parsed = merge ? [self.firestore.dataConverter parsedMergeData:documentData] - : [self.firestore.dataConverter parsedSetData:documentData]; + FSTParsedSetData *parsed = + merge ? [self.firestore.dataConverter parsedMergeData:documentData fieldMask:nil] + : [self.firestore.dataConverter parsedSetData:documentData]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key precondition:Precondition::None()] + completion:completion]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData + mergeFields:(NSArray<id> *)mergeFields + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedMergeData:documentData fieldMask:mergeFields]; return [self.firestore.client writeMutations:[parsed mutationsWithKey:self.key precondition:Precondition::None()] completion:completion]; diff --git a/Firestore/Source/API/FIRFieldPath.mm b/Firestore/Source/API/FIRFieldPath.mm index d0d8714..4fd0022 100644 --- a/Firestore/Source/API/FIRFieldPath.mm +++ b/Firestore/Source/API/FIRFieldPath.mm @@ -25,6 +25,7 @@ #import "Firestore/Source/Util/FSTUsageValidation.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; @@ -114,7 +115,7 @@ NS_ASSUME_NONNULL_BEGIN } - (NSUInteger)hash { - return _internalValue.Hash(); + return util::Hash(_internalValue); } - (const firebase::firestore::model::FieldPath &)internalValue { diff --git a/Firestore/Source/API/FIRFirestoreVersion.h b/Firestore/Source/API/FIRFirestoreVersion.h index 6fb21eb..aaf7976 100644 --- a/Firestore/Source/API/FIRFirestoreVersion.h +++ b/Firestore/Source/API/FIRFirestoreVersion.h @@ -19,4 +19,4 @@ #import <Foundation/Foundation.h> /** Version string for the Firebase Firestore SDK. */ -FOUNDATION_EXPORT const unsigned char *const FirebaseFirestoreVersionString; +FOUNDATION_EXPORT const unsigned char *const FIRFirestoreVersionString; diff --git a/Firestore/Source/API/FIRFirestoreVersion.mm b/Firestore/Source/API/FIRFirestoreVersion.mm index 8ebe814..8f0428c 100644 --- a/Firestore/Source/API/FIRFirestoreVersion.mm +++ b/Firestore/Source/API/FIRFirestoreVersion.mm @@ -27,5 +27,5 @@ #define STR(x) STR_EXPAND(x) #define STR_EXPAND(x) #x -extern "C" const unsigned char *const FirebaseFirestoreVersionString = +extern "C" const unsigned char *const FIRFirestoreVersionString = (const unsigned char *const)STR(FIRFirestore_VERSION); diff --git a/Firestore/Source/API/FIRTransaction.mm b/Firestore/Source/API/FIRTransaction.mm index 668a359..b5bdefa 100644 --- a/Firestore/Source/API/FIRTransaction.mm +++ b/Firestore/Source/API/FIRTransaction.mm @@ -68,8 +68,19 @@ NS_ASSUME_NONNULL_BEGIN forDocument:(FIRDocumentReference *)document merge:(BOOL)merge { [self validateReference:document]; - FSTParsedSetData *parsed = merge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; + FSTParsedSetData *parsed = merge + ? [self.firestore.dataConverter parsedMergeData:data fieldMask:nil] + : [self.firestore.dataConverter parsedSetData:data]; + [self.internalTransaction setData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields { + [self validateReference:document]; + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedMergeData:data fieldMask:mergeFields]; [self.internalTransaction setData:parsed forDocument:document.key]; return self; } diff --git a/Firestore/Source/API/FIRWriteBatch.mm b/Firestore/Source/API/FIRWriteBatch.mm index 1185dae..366c708 100644 --- a/Firestore/Source/API/FIRWriteBatch.mm +++ b/Firestore/Source/API/FIRWriteBatch.mm @@ -70,8 +70,21 @@ NS_ASSUME_NONNULL_BEGIN merge:(BOOL)merge { [self verifyNotCommitted]; [self validateReference:document]; - FSTParsedSetData *parsed = merge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; + FSTParsedSetData *parsed = merge + ? [self.firestore.dataConverter parsedMergeData:data fieldMask:nil] + : [self.firestore.dataConverter parsedSetData:data]; + [self.mutations + addObjectsFromArray:[parsed mutationsWithKey:document.key precondition:Precondition::None()]]; + return self; +} + +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedMergeData:data fieldMask:mergeFields]; [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key precondition:Precondition::None()]]; return self; diff --git a/Firestore/Source/API/FSTUserDataConverter.h b/Firestore/Source/API/FSTUserDataConverter.h index 98a65ae..27a5f09 100644 --- a/Firestore/Source/API/FSTUserDataConverter.h +++ b/Firestore/Source/API/FSTUserDataConverter.h @@ -27,7 +27,6 @@ @class FSTObjectValue; @class FSTFieldValue; @class FSTMutation; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -130,7 +129,7 @@ typedef id _Nullable (^FSTPreConverterBlock)(id _Nullable); - (FSTParsedSetData *)parsedSetData:(id)input; /** Parse document data from a setData call with `merge:YES`. */ -- (FSTParsedSetData *)parsedMergeData:(id)input; +- (FSTParsedSetData *)parsedMergeData:(id)input fieldMask:(nullable NSArray<id> *)fieldMask; /** Parse update data from an updateData call. */ - (FSTParsedUpdateData *)parsedUpdateData:(id)input; diff --git a/Firestore/Source/API/FSTUserDataConverter.mm b/Firestore/Source/API/FSTUserDataConverter.mm index 2794398..6d01c75 100644 --- a/Firestore/Source/API/FSTUserDataConverter.mm +++ b/Firestore/Source/API/FSTUserDataConverter.mm @@ -412,7 +412,7 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { return self; } -- (FSTParsedSetData *)parsedMergeData:(id)input { +- (FSTParsedSetData *)parsedMergeData:(id)input fieldMask:(nullable NSArray<id> *)fieldMask { // 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]]) { @@ -424,9 +424,45 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { path:absl::make_unique<FieldPath>(FieldPath::EmptyPath())]; FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; + FieldMask convertedFieldMask; + std::vector<FieldTransform> convertedFieldTransform; + + if (fieldMask) { + __block std::vector<FieldPath> fieldMaskPaths{}; + [fieldMask enumerateObjectsUsingBlock:^(id fieldPath, NSUInteger idx, BOOL *stop) { + FieldPath path{}; + + if ([fieldPath isKindOfClass:[NSString class]]) { + path = [FIRFieldPath pathWithDotSeparatedString:fieldPath].internalValue; + } else if ([fieldPath isKindOfClass:[FIRFieldPath class]]) { + path = ((FIRFieldPath *)fieldPath).internalValue; + } else { + FSTThrowInvalidArgument( + @"All elements in mergeFields: must be NSStrings or FIRFieldPaths."); + } + + if ([updateData valueForPath:path] == nil) { + FSTThrowInvalidArgument( + @"Field '%s' is specified in your field mask but missing from your input data.", + path.CanonicalString().c_str()); + } + + fieldMaskPaths.push_back(path); + }]; + convertedFieldMask = FieldMask(fieldMaskPaths); + std::copy_if(context.fieldTransforms->begin(), context.fieldTransforms->end(), + std::back_inserter(convertedFieldTransform), + [&](const FieldTransform &fieldTransform) { + return convertedFieldMask.covers(fieldTransform.path()); + }); + } else { + convertedFieldMask = FieldMask{*context.fieldMask}; + convertedFieldTransform = *context.fieldTransforms; + } + return [[FSTParsedSetData alloc] initWithData:updateData - fieldMask:FieldMask{*context.fieldMask} - fieldTransforms:*context.fieldTransforms]; + fieldMask:convertedFieldMask + fieldTransforms:convertedFieldTransform]; } - (FSTParsedSetData *)parsedSetData:(id)input { diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm index 4f1a20b..658cf57 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -56,6 +56,7 @@ using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -311,11 +312,9 @@ NS_ASSUME_NONNULL_BEGIN completion:(void (^)(FIRQuerySnapshot *_Nullable query, NSError *_Nullable error))completion { [self.workerDispatchQueue dispatchAsync:^{ - FSTDocumentDictionary *docs = [self.localStore executeQuery:query.query]; - FSTDocumentKeySet *remoteKeys = [FSTDocumentKeySet keySet]; - FSTView *view = [[FSTView alloc] initWithQuery:query.query remoteDocuments:remoteKeys]; + FSTView *view = [[FSTView alloc] initWithQuery:query.query remoteDocuments:DocumentKeySet{}]; FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs]; FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; FSTAssert(viewChange.limboChanges.count == 0, diff --git a/Firestore/Source/Core/FSTQuery.mm b/Firestore/Source/Core/FSTQuery.mm index 0cd11e8..d3961e8 100644 --- a/Firestore/Source/Core/FSTQuery.mm +++ b/Firestore/Source/Core/FSTQuery.mm @@ -28,6 +28,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; @@ -259,7 +260,7 @@ NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOpe } - (NSUInteger)hash { - return _field.Hash(); + return util::Hash(_field); } @end @@ -305,7 +306,7 @@ NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOpe } - (NSUInteger)hash { - return _field.Hash(); + return util::Hash(_field); } @end diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm index 138fb41..ed97d6c 100644 --- a/Firestore/Source/Core/FSTSyncEngine.mm +++ b/Firestore/Source/Core/FSTSyncEngine.mm @@ -21,10 +21,10 @@ #include <map> #include <set> #include <unordered_map> +#include <utility> #import "FIRFirestoreErrors.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Core/FSTTransaction.h" #import "Firestore/Source/Core/FSTView.h" #import "Firestore/Source/Core/FSTViewSnapshot.h" @@ -45,12 +45,15 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; using firebase::firestore::core::TargetIdGenerator; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -184,9 +187,9 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1; FSTQueryData *queryData = [self.localStore allocateQuery:query]; FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; - FSTDocumentKeySet *remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID]; + DocumentKeySet remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:remoteKeys]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:std::move(remoteKeys)]; FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs]; FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; FSTAssert(viewChange.limboChanges.count == 0, @@ -301,8 +304,7 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1; if (iter == self->_limboKeysByTarget.end()) { FSTQueryView *qv = self.queryViewsByTarget[targetID]; FSTAssert(qv, @"Missing queryview for non-limbo query: %i", [targetID intValue]); - [remoteEvent filterUpdatesFromTargetChange:targetChange - existingDocuments:qv.view.syncedDocuments]; + [targetChange.mapping filterUpdatesUsingExistingKeys:qv.view.syncedDocuments]; } else { [remoteEvent synthesizeDeleteForLimboTargetChange:targetChange key:iter->second]; } @@ -346,10 +348,15 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1; NSMutableDictionary<NSNumber *, FSTTargetChange *> *targetChanges = [NSMutableDictionary dictionary]; FSTDeletedDocument *doc = - [FSTDeletedDocument documentWithKey:limboKey version:[FSTSnapshotVersion noVersion]]; - FSTRemoteEvent *event = [FSTRemoteEvent eventWithSnapshotVersion:[FSTSnapshotVersion noVersion] - targetChanges:targetChanges - documentUpdates:{{limboKey, doc}}]; + [FSTDeletedDocument documentWithKey:limboKey version:SnapshotVersion::None()]; + DocumentKeySet limboDocuments = DocumentKeySet{doc.key}; + FSTRemoteEvent *event = + [[FSTRemoteEvent alloc] initWithSnapshotVersion:SnapshotVersion::None() + targetChanges:targetChanges + documentUpdates:{ + { limboKey, doc } + } + limboDocuments:std::move(limboDocuments)]; [self applyRemoteEvent:event]; } else { FSTQueryView *queryView = self.queryViewsByTarget[@(targetID)]; diff --git a/Firestore/Source/Core/FSTTransaction.mm b/Firestore/Source/Core/FSTTransaction.mm index 4aabd5a..5c36b20 100644 --- a/Firestore/Source/Core/FSTTransaction.mm +++ b/Firestore/Source/Core/FSTTransaction.mm @@ -23,19 +23,21 @@ #import "FIRFirestoreErrors.h" #import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTDatastore.h" #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTUsageValidation.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" using firebase::firestore::model::DocumentKey; using firebase::firestore::model::Precondition; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -53,7 +55,7 @@ NS_ASSUME_NONNULL_BEGIN @end @implementation FSTTransaction { - std::map<DocumentKey, FSTSnapshotVersion *> _readVersions; + std::map<DocumentKey, SnapshotVersion> _readVersions; } + (instancetype)transactionWithDatastore:(FSTDatastore *)datastore { @@ -79,11 +81,11 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error { FSTAssert(error != nil, @"nil error parameter"); *error = nil; - FSTSnapshotVersion *docVersion = doc.version; + SnapshotVersion 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]; + docVersion = SnapshotVersion::None(); } if (_readVersions.find(doc.key) == _readVersions.end()) { _readVersions[doc.key] = docVersion; @@ -159,8 +161,8 @@ NS_ASSUME_NONNULL_BEGIN return Precondition::Exists(true); } - FSTSnapshotVersion *version = iter->second; - if ([version isEqual:[FSTSnapshotVersion noVersion]]) { + const SnapshotVersion &version = iter->second; + if (version == SnapshotVersion::None()) { // The document was read, but doesn't exist. // Return an error because the precondition is impossible if (error) { @@ -200,7 +202,7 @@ NS_ASSUME_NONNULL_BEGIN 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 without timestamp. - _readVersions[key] = [FSTSnapshotVersion noVersion]; + _readVersions[key] = SnapshotVersion::None(); } - (void)commitWithCompletion:(FSTVoidErrorBlock)completion { @@ -215,15 +217,15 @@ NS_ASSUME_NONNULL_BEGIN } // Make a list of read documents that haven't been written. - FSTDocumentKeySet *unwritten = [FSTDocumentKeySet keySet]; + DocumentKeySet unwritten; for (const auto &kv : _readVersions) { - unwritten = [unwritten setByAddingObject:kv.first]; + unwritten = unwritten.insert(kv.first); }; // For each mutation, note that the doc was written. for (FSTMutation *mutation in self.mutations) { - unwritten = [unwritten setByRemovingObject:mutation.key]; + unwritten = unwritten.erase(mutation.key); } - if (unwritten.count) { + if (!unwritten.empty()) { // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend. completion([NSError errorWithDomain:FIRFirestoreErrorDomain diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h index 431b863..fc6cead 100644 --- a/Firestore/Source/Core/FSTView.h +++ b/Firestore/Source/Core/FSTView.h @@ -18,9 +18,9 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @class FSTDocumentSet; @class FSTDocumentViewChangeSet; @@ -38,6 +38,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; +- (const firebase::firestore::model::DocumentKeySet &)mutatedKeys; + /** The new set of docs that should be in the view. */ @property(nonatomic, strong, readonly) FSTDocumentSet *documentSet; @@ -50,8 +52,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, assign, readonly) BOOL needsRefill; -@property(nonatomic, strong, readonly) FSTDocumentKeySet *mutatedKeys; - @end #pragma mark - FSTLimboDocumentChange @@ -97,7 +97,8 @@ typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithQuery:(FSTQuery *)query - remoteDocuments:(FSTDocumentKeySet *)remoteDocuments NS_DESIGNATED_INITIALIZER; + remoteDocuments:(firebase::firestore::model::DocumentKeySet)remoteDocuments + NS_DESIGNATED_INITIALIZER; /** * Iterates over a set of doc changes, applies the query limit, and computes what the new results @@ -152,7 +153,7 @@ typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { * The set of remote documents that the server has told us belongs to the target associated with * this view. */ -@property(nonatomic, strong, readonly) FSTDocumentKeySet *syncedDocuments; +- (const firebase::firestore::model::DocumentKeySet &)syncedDocuments; @end diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm index d87951a..d254a82 100644 --- a/Firestore/Source/Core/FSTView.mm +++ b/Firestore/Source/Core/FSTView.mm @@ -29,6 +29,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -40,26 +41,32 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet changeSet:(FSTDocumentViewChangeSet *)changeSet needsRefill:(BOOL)needsRefill - mutatedKeys:(FSTDocumentKeySet *)mutatedKeys NS_DESIGNATED_INITIALIZER; + mutatedKeys:(DocumentKeySet)mutatedKeys NS_DESIGNATED_INITIALIZER; @end -@implementation FSTViewDocumentChanges +@implementation FSTViewDocumentChanges { + DocumentKeySet _mutatedKeys; +} - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet changeSet:(FSTDocumentViewChangeSet *)changeSet needsRefill:(BOOL)needsRefill - mutatedKeys:(FSTDocumentKeySet *)mutatedKeys { + mutatedKeys:(DocumentKeySet)mutatedKeys { self = [super init]; if (self) { _documentSet = documentSet; _changeSet = changeSet; _needsRefill = needsRefill; - _mutatedKeys = mutatedKeys; + _mutatedKeys = std::move(mutatedKeys); } return self; } +- (const DocumentKeySet &)mutatedKeys { + return _mutatedKeys; +} + @end #pragma mark - FSTLimboDocumentChange @@ -165,32 +172,33 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang @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; +@end -/** Document Keys that have local changes. */ -@property(nonatomic, strong) FSTDocumentKeySet *mutatedKeys; +@implementation FSTView { + /** Documents included in the remote target. */ + DocumentKeySet _syncedDocuments; -@end + /** Documents in the view but not in the remote target */ + DocumentKeySet _limboDocuments; -@implementation FSTView + /** Document Keys that have local changes. */ + DocumentKeySet _mutatedKeys; +} -- (instancetype)initWithQuery:(FSTQuery *)query - remoteDocuments:(nonnull FSTDocumentKeySet *)remoteDocuments { +- (instancetype)initWithQuery:(FSTQuery *)query remoteDocuments:(DocumentKeySet)remoteDocuments { self = [super init]; if (self) { _query = query; _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator]; - _syncedDocuments = remoteDocuments; - _limboDocuments = [FSTDocumentKeySet keySet]; - _mutatedKeys = [FSTDocumentKeySet keySet]; + _syncedDocuments = std::move(remoteDocuments); } return self; } +- (const DocumentKeySet &)syncedDocuments { + return _syncedDocuments; +} + - (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges { return [self computeChangesWithDocuments:docChanges previousChanges:nil]; } @@ -202,8 +210,8 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet]; FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet; - __block FSTDocumentKeySet *newMutatedKeys = - previousChanges ? previousChanges.mutatedKeys : self.mutatedKeys; + __block DocumentKeySet newMutatedKeys = + previousChanges ? previousChanges.mutatedKeys : _mutatedKeys; __block FSTDocumentSet *newDocumentSet = oldDocumentSet; __block BOOL needsRefill = NO; @@ -236,13 +244,13 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang if (newDoc) { newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc]; if (newDoc.hasLocalMutations) { - newMutatedKeys = [newMutatedKeys setByAddingObject:key]; + newMutatedKeys = newMutatedKeys.insert(key); } else { - newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + newMutatedKeys = newMutatedKeys.erase(key); } } else { newDocumentSet = [newDocumentSet documentSetByRemovingKey:key]; - newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + newMutatedKeys = newMutatedKeys.erase(key); } // Calculate change @@ -311,7 +319,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang FSTDocumentSet *oldDocuments = self.documentSet; self.documentSet = docChanges.documentSet; - self.mutatedKeys = docChanges.mutatedKeys; + _mutatedKeys = docChanges.mutatedKeys; // Sort changes based on type and query comparator. NSArray<FSTDocumentViewChange *> *changes = [docChanges.changeSet changes]; @@ -325,7 +333,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang }]; [self applyTargetChange:targetChange]; NSArray<FSTLimboDocumentChange *> *limboChanges = [self updateLimboDocuments]; - BOOL synced = self.limboDocuments.count == 0 && self.isCurrent; + BOOL synced = _limboDocuments.empty() && self.isCurrent; FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal; BOOL syncStateChanged = newSyncState != self.syncState; self.syncState = newSyncState; @@ -340,7 +348,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang oldDocuments:oldDocuments documentChanges:changes fromCache:newSyncState == FSTSyncStateLocal - hasPendingWrites:!docChanges.mutatedKeys.isEmpty + hasPendingWrites:!docChanges.mutatedKeys.empty() syncStateChanged:syncStateChanged]; return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges]; @@ -358,7 +366,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang initWithDocumentSet:self.documentSet changeSet:[FSTDocumentViewChangeSet changeSet] needsRefill:NO - mutatedKeys:self.mutatedKeys]]; + mutatedKeys:_mutatedKeys]]; } else { // No effect, just return a no-op FSTViewChange. return [[FSTViewChange alloc] initWithSnapshot:nil limboChanges:@[]]; @@ -370,7 +378,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang /** Returns whether the doc for the given key should be in limbo. */ - (BOOL)shouldBeLimboDocumentKey:(const DocumentKey &)key { // If the remote end says it's part of this query, it's not in limbo. - if ([self.syncedDocuments containsObject:key]) { + if (_syncedDocuments.contains(key)) { return NO; } // The local store doesn't think it's a result, so it shouldn't be in limbo. @@ -395,16 +403,14 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang if (targetChange) { FSTTargetMapping *targetMapping = targetChange.mapping; if ([targetMapping isKindOfClass:[FSTResetMapping class]]) { - self.syncedDocuments = ((FSTResetMapping *)targetMapping).documents; + _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]; - }]; + for (const DocumentKey &key : ((FSTUpdateMapping *)targetMapping).addedDocuments) { + _syncedDocuments = _syncedDocuments.insert(key); + } + for (const DocumentKey &key : ((FSTUpdateMapping *)targetMapping).removedDocuments) { + _syncedDocuments = _syncedDocuments.erase(key); + } } switch (targetChange.currentStatusUpdate) { @@ -428,29 +434,29 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang } // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents. - FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments; - self.limboDocuments = [FSTDocumentKeySet keySet]; + DocumentKeySet oldLimboDocuments = std::move(_limboDocuments); + _limboDocuments = DocumentKeySet{}; for (FSTDocument *doc in self.documentSet.documentEnumerator) { if ([self shouldBeLimboDocumentKey:doc.key]) { - self.limboDocuments = [self.limboDocuments setByAddingObject:doc.key]; + _limboDocuments = _limboDocuments.insert(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]) { + [NSMutableArray arrayWithCapacity:(oldLimboDocuments.size() + _limboDocuments.size())]; + for (const DocumentKey &key : oldLimboDocuments) { + if (!_limboDocuments.contains(key)) { [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:key]]; } - }]; - [self.limboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - if (![oldLimboDocuments containsObject:key]) { + } + for (const DocumentKey &key : _limboDocuments) { + if (!oldLimboDocuments.contains(key)) { [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:key]]; } - }]; + } return changes; } diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm index fae85e7..bc2f2eb 100644 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -243,6 +243,10 @@ using leveldb::WriteOptions; _ptr.reset(); } +- (_Nullable id<FSTReferenceDelegate>)referenceDelegate { + return nil; +} + #pragma mark - Error and Status + (nullable NSError *)errorWithStatus:(Status)status description:(NSString *)description, ... { diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm index 75c3cf6..2c9f68d 100644 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm @@ -511,6 +511,7 @@ using leveldb::WriteOptions; documentKey:mutation.key batchID:batchID]; _db.currentTransaction->Delete(key); + [_db.referenceDelegate removeMutationReference:mutation.key]; [garbageCollector addPotentialGarbageKey:mutation.key]; } } diff --git a/Firestore/Source/Local/FSTLevelDBQueryCache.mm b/Firestore/Source/Local/FSTLevelDBQueryCache.mm index 5fde7d7..68b6f98 100644 --- a/Firestore/Source/Local/FSTLevelDBQueryCache.mm +++ b/Firestore/Source/Local/FSTLevelDBQueryCache.mm @@ -18,6 +18,7 @@ #include <memory> #include <string> +#include <utility> #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" #import "Firestore/Source/Core/FSTQuery.h" @@ -28,6 +29,7 @@ #import "Firestore/Source/Util/FSTAssert.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "absl/strings/match.h" NS_ASSUME_NONNULL_BEGIN @@ -35,9 +37,11 @@ NS_ASSUME_NONNULL_BEGIN using firebase::firestore::local::LevelDbTransaction; using Firestore::StringView; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; using leveldb::DB; using leveldb::Slice; using leveldb::Status; +using firebase::firestore::model::DocumentKeySet; @interface FSTLevelDBQueryCache () @@ -55,7 +59,7 @@ using leveldb::Status; * The last received snapshot version. This is part of `metadata` but we store it separately to * avoid extra conversion to/from GPBTimestamp. */ - FSTSnapshotVersion *_lastRemoteSnapshotVersion; + SnapshotVersion _lastRemoteSnapshotVersion; } + (nullable FSTPBTargetGlobal *)readTargetMetadataWithTransaction: @@ -135,13 +139,14 @@ using leveldb::Status; return self.metadata.highestListenSequenceNumber; } -- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { +- (const SnapshotVersion &)lastRemoteSnapshotVersion { return _lastRemoteSnapshotVersion; } -- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion { - _lastRemoteSnapshotVersion = snapshotVersion; - self.metadata.lastRemoteSnapshotVersion = [self.serializer encodedVersion:snapshotVersion]; +- (void)setLastRemoteSnapshotVersion:(SnapshotVersion)snapshotVersion { + _lastRemoteSnapshotVersion = std::move(snapshotVersion); + self.metadata.lastRemoteSnapshotVersion = + [self.serializer encodedVersion:_lastRemoteSnapshotVersion]; _db.currentTransaction->Put([FSTLevelDBTargetGlobalKey key], self.metadata); } @@ -278,30 +283,30 @@ using leveldb::Status; #pragma mark Matching Key tracking -- (void)addMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID { +- (void)addMatchingKeys:(const DocumentKeySet &)keys forTargetID:(FSTTargetID)targetID { // 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) { + for (const DocumentKey &key : keys) { self->_db.currentTransaction->Put( - [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:documentKey], - emptyBuffer); + [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key], emptyBuffer); self->_db.currentTransaction->Put( - [FSTLevelDBDocumentTargetKey keyWithDocumentKey:documentKey targetID:targetID], - emptyBuffer); - }]; + [FSTLevelDBDocumentTargetKey keyWithDocumentKey:key targetID:targetID], emptyBuffer); + [self->_db.referenceDelegate addReference:key target:targetID]; + }; } -- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID { - [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { +- (void)removeMatchingKeys:(const DocumentKeySet &)keys forTargetID:(FSTTargetID)targetID { + for (const DocumentKey &key : keys) { self->_db.currentTransaction->Delete( [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key]); self->_db.currentTransaction->Delete( [FSTLevelDBDocumentTargetKey keyWithDocumentKey:key targetID:targetID]); + [self->_db.referenceDelegate removeReference:key target:targetID]; [self.garbageCollector addPotentialGarbageKey:key]; - }]; + } } - (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID { @@ -327,12 +332,12 @@ using leveldb::Status; } } -- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { +- (DocumentKeySet)matchingKeysForTargetID:(FSTTargetID)targetID { std::string indexPrefix = [FSTLevelDBTargetDocumentKey keyPrefixWithTargetID:targetID]; auto indexIterator = _db.currentTransaction->NewIterator(); indexIterator->Seek(indexPrefix); - FSTDocumentKeySet *result = [FSTDocumentKeySet keySet]; + DocumentKeySet result; FSTLevelDBTargetDocumentKey *rowKey = [[FSTLevelDBTargetDocumentKey alloc] init]; for (; indexIterator->Valid(); indexIterator->Next()) { absl::string_view indexKey = indexIterator->key(); @@ -342,7 +347,7 @@ using leveldb::Status; break; } - result = [result setByAddingObject:rowKey.documentKey]; + result = result.insert(rowKey.documentKey); } return result; diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.h b/Firestore/Source/Local/FSTLocalDocumentsView.h index e75e0f3..bb5bb22 100644 --- a/Firestore/Source/Local/FSTLocalDocumentsView.h +++ b/Firestore/Source/Local/FSTLocalDocumentsView.h @@ -17,9 +17,9 @@ #import <Foundation/Foundation.h> #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @class FSTMaybeDocument; @class FSTQuery; @@ -53,7 +53,8 @@ NS_ASSUME_NONNULL_BEGIN * 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; +- (FSTMaybeDocumentDictionary *)documentsForKeys: + (const firebase::firestore::model::DocumentKeySet &)keys; /** Performs a query against the local view of all documents. */ - (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query; diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.mm b/Firestore/Source/Local/FSTLocalDocumentsView.mm index e9b9423..471840a 100644 --- a/Firestore/Source/Local/FSTLocalDocumentsView.mm +++ b/Firestore/Source/Local/FSTLocalDocumentsView.mm @@ -17,7 +17,6 @@ #import "Firestore/Source/Local/FSTLocalDocumentsView.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Local/FSTRemoteDocumentCache.h" #import "Firestore/Source/Model/FSTDocument.h" @@ -28,9 +27,12 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" using firebase::firestore::model::DocumentKey; using firebase::firestore::model::ResourcePath; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -64,14 +66,14 @@ NS_ASSUME_NONNULL_BEGIN return [self localDocument:remoteDoc key:key]; } -- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys { +- (FSTMaybeDocumentDictionary *)documentsForKeys:(const DocumentKeySet &)keys { FSTMaybeDocumentDictionary *results = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; - for (FSTDocumentKey *key in keys.objectEnumerator) { + for (const DocumentKey &key : keys) { // 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]]; + maybeDoc = [FSTDeletedDocument documentWithKey:key version:SnapshotVersion::None()]; } results = [results dictionaryBySettingObject:maybeDoc forKey:key]; } @@ -105,7 +107,7 @@ NS_ASSUME_NONNULL_BEGIN // Now use the mutation queue to discover any other documents that may match the query after // applying mutations. - FSTDocumentKeySet *matchingKeys = [FSTDocumentKeySet keySet]; + DocumentKeySet matchingKeys; NSArray<FSTMutationBatch *> *matchingMutationBatches = [self.mutationQueue allMutationBatchesAffectingQuery:query]; for (FSTMutationBatch *batch in matchingMutationBatches) { @@ -114,13 +116,13 @@ NS_ASSUME_NONNULL_BEGIN // If the key is already in the results, we can skip it. if (![results containsKey:mutation.key]) { - matchingKeys = [matchingKeys setByAddingObject:mutation.key]; + matchingKeys = matchingKeys.insert(mutation.key); } } } // Now add in results for the matchingKeys. - for (FSTDocumentKey *key in matchingKeys.objectEnumerator) { + for (const DocumentKey &key : matchingKeys) { FSTMaybeDocument *doc = [self documentForKey:key]; if ([doc isKindOfClass:[FSTDocument class]]) { results = [results dictionaryBySettingObject:(FSTDocument *)doc forKey:key]; diff --git a/Firestore/Source/Local/FSTLocalSerializer.h b/Firestore/Source/Local/FSTLocalSerializer.h index 6ca7f01..b75f3e6 100644 --- a/Firestore/Source/Local/FSTLocalSerializer.h +++ b/Firestore/Source/Local/FSTLocalSerializer.h @@ -16,11 +16,12 @@ #import <Foundation/Foundation.h> +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + @class FSTMaybeDocument; @class FSTMutationBatch; @class FSTQueryData; @class FSTSerializerBeta; -@class FSTSnapshotVersion; @class FSTPBMaybeDocument; @class FSTPBTarget; @@ -61,11 +62,11 @@ NS_ASSUME_NONNULL_BEGIN /** 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; +/** Encodes a SnapshotVersion model into a GPBTimestamp proto. */ +- (GPBTimestamp *)encodedVersion:(const firebase::firestore::model::SnapshotVersion &)version; -/** Decodes a GPBTimestamp proto into a FSTSnapshotVersion model. */ -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version; +/** Decodes a GPBTimestamp proto into a SnapshotVersion model. */ +- (firebase::firestore::model::SnapshotVersion)decodedVersion:(GPBTimestamp *)version; @end diff --git a/Firestore/Source/Local/FSTLocalSerializer.mm b/Firestore/Source/Local/FSTLocalSerializer.mm index 61e173a..8fa1278 100644 --- a/Firestore/Source/Local/FSTLocalSerializer.mm +++ b/Firestore/Source/Local/FSTLocalSerializer.mm @@ -30,9 +30,13 @@ #import "Firestore/Source/Remote/FSTSerializerBeta.h" #import "Firestore/Source/Util/FSTAssert.h" +#include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +using firebase::Timestamp; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; @interface FSTLocalSerializer () @@ -99,8 +103,8 @@ using firebase::firestore::model::DocumentKey; FSTSerializerBeta *remoteSerializer = self.remoteSerializer; FSTObjectValue *data = [remoteSerializer decodedFields:document.fields]; - const DocumentKey key = [remoteSerializer decodedDocumentKey:document.name]; - FSTSnapshotVersion *version = [remoteSerializer decodedVersion:document.updateTime]; + DocumentKey key = [remoteSerializer decodedDocumentKey:document.name]; + SnapshotVersion version = [remoteSerializer decodedVersion:document.updateTime]; return [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; } @@ -118,8 +122,8 @@ using firebase::firestore::model::DocumentKey; - (FSTDeletedDocument *)decodedDeletedDocument:(FSTPBNoDocument *)proto { FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - const DocumentKey key = [remoteSerializer decodedDocumentKey:proto.name]; - FSTSnapshotVersion *version = [remoteSerializer decodedVersion:proto.readTime]; + DocumentKey key = [remoteSerializer decodedDocumentKey:proto.name]; + SnapshotVersion version = [remoteSerializer decodedVersion:proto.readTime]; return [FSTDeletedDocument documentWithKey:key version:version]; } @@ -128,7 +132,8 @@ using firebase::firestore::model::DocumentKey; FSTPBWriteBatch *proto = [FSTPBWriteBatch message]; proto.batchId = batch.batchID; - proto.localWriteTime = [remoteSerializer encodedTimestamp:batch.localWriteTime]; + proto.localWriteTime = [remoteSerializer + encodedTimestamp:Timestamp{batch.localWriteTime.seconds, batch.localWriteTime.nanoseconds}]; NSMutableArray<GCFSWrite *> *writes = proto.writesArray; for (FSTMutation *mutation in batch.mutations) { @@ -146,11 +151,13 @@ using firebase::firestore::model::DocumentKey; [mutations addObject:[remoteSerializer decodedMutation:write]]; } - FIRTimestamp *localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; + Timestamp localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; - return [[FSTMutationBatch alloc] initWithBatchID:batchID - localWriteTime:localWriteTime - mutations:mutations]; + return [[FSTMutationBatch alloc] + initWithBatchID:batchID + localWriteTime:[FIRTimestamp timestampWithSeconds:localWriteTime.seconds() + nanoseconds:localWriteTime.nanoseconds()] + mutations:mutations]; } - (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { @@ -181,7 +188,7 @@ using firebase::firestore::model::DocumentKey; FSTTargetID targetID = target.targetId; FSTListenSequenceNumber sequenceNumber = target.lastListenSequenceNumber; - FSTSnapshotVersion *version = [remoteSerializer decodedVersion:target.snapshotVersion]; + SnapshotVersion version = [remoteSerializer decodedVersion:target.snapshotVersion]; NSData *resumeToken = target.resumeToken; FSTQuery *query; @@ -206,11 +213,11 @@ using firebase::firestore::model::DocumentKey; resumeToken:resumeToken]; } -- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { +- (GPBTimestamp *)encodedVersion:(const SnapshotVersion &)version { return [self.remoteSerializer encodedVersion:version]; } -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { +- (SnapshotVersion)decodedVersion:(GPBTimestamp *)version { return [self.remoteSerializer decodedVersion:version]; } diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h index 82402e4..1f4146a 100644 --- a/Firestore/Source/Local/FSTLocalStore.h +++ b/Firestore/Source/Local/FSTLocalStore.h @@ -18,11 +18,11 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" -#import "Firestore/Source/Model/FSTDocumentVersionDictionary.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTLocalViewChanges; @class FSTLocalWriteResult; @@ -141,7 +141,7 @@ NS_ASSUME_NONNULL_BEGIN * Returns the last consistent snapshot processed (used by the RemoteStore to determine whether to * buffer incoming snapshots from the backend). */ -- (FSTSnapshotVersion *)lastRemoteSnapshotVersion; +- (const firebase::firestore::model::SnapshotVersion &)lastRemoteSnapshotVersion; /** * Updates the "ground-state" (remote) documents. We assume that the remote event reflects any @@ -156,7 +156,7 @@ NS_ASSUME_NONNULL_BEGIN * Returns the keys of the documents that are associated with the given targetID in the remote * table. */ -- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID; +- (firebase::firestore::model::DocumentKeySet)remoteDocumentKeysForTarget:(FSTTargetID)targetID; /** * Collects garbage if necessary. diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index b5dfeec..0d6a785 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -21,7 +21,6 @@ #import "FIRTimestamp.h" #import "Firestore/Source/Core/FSTListenSequence.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTGarbageCollector.h" #import "Firestore/Source/Local/FSTLocalDocumentsView.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" @@ -43,11 +42,14 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" using firebase::firestore::auth::User; -using firebase::firestore::model::DocumentKey; using firebase::firestore::core::TargetIdGenerator; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentVersionMap; NS_ASSUME_NONNULL_BEGIN @@ -112,6 +114,7 @@ NS_ASSUME_NONNULL_BEGIN _localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:_remoteDocumentCache mutationQueue:_mutationQueue]; _localViewReferences = [[FSTReferenceSet alloc] init]; + [_persistence.referenceDelegate addInMemoryPins:_localViewReferences]; _garbageCollector = garbageCollector; [_garbageCollector addGarbageSource:_queryCache]; @@ -186,11 +189,11 @@ NS_ASSUME_NONNULL_BEGIN mutationQueue:self.mutationQueue]; // Union the old/new changed keys. - FSTDocumentKeySet *changedKeys = [FSTDocumentKeySet keySet]; + DocumentKeySet changedKeys; for (NSArray<FSTMutationBatch *> *batches in @[ oldBatches, newBatches ]) { for (FSTMutationBatch *batch in batches) { for (FSTMutation *mutation in batch.mutations) { - changedKeys = [changedKeys setByAddingObject:mutation.key]; + changedKeys = changedKeys.insert(mutation.key); } } } @@ -205,7 +208,7 @@ NS_ASSUME_NONNULL_BEGIN FIRTimestamp *localWriteTime = [FIRTimestamp timestamp]; FSTMutationBatch *batch = [self.mutationQueue addMutationBatchWithWriteTime:localWriteTime mutations:mutations]; - FSTDocumentKeySet *keys = [batch keys]; + DocumentKeySet keys = [batch keys]; FSTMaybeDocumentDictionary *changedDocuments = [self.localDocuments documentsForKeys:keys]; return [FSTLocalWriteResult resultForBatchID:batch.batchID changes:changedDocuments]; }); @@ -217,10 +220,9 @@ NS_ASSUME_NONNULL_BEGIN [mutationQueue acknowledgeBatch:batchResult.batch streamToken:batchResult.streamToken]; - FSTDocumentKeySet *affected; + DocumentKeySet affected; if ([self shouldHoldBatchResultWithVersion:batchResult.commitVersion]) { [self.heldBatchResults addObject:batchResult]; - affected = [FSTDocumentKeySet keySet]; } else { affected = [self releaseBatchResults:@[ batchResult ]]; } @@ -239,7 +241,7 @@ NS_ASSUME_NONNULL_BEGIN FSTBatchID lastAcked = [self.mutationQueue highestAcknowledgedBatchID]; FSTAssert(batchID > lastAcked, @"Acknowledged batches can't be rejected."); - FSTDocumentKeySet *affected = [self removeMutationBatch:toReject]; + DocumentKeySet affected = [self removeMutationBatch:toReject]; [self.mutationQueue performConsistencyCheck]; @@ -256,12 +258,14 @@ NS_ASSUME_NONNULL_BEGIN [&]() { [self.mutationQueue setLastStreamToken:streamToken]; }); } -- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { +- (const SnapshotVersion &)lastRemoteSnapshotVersion { return [self.queryCache lastRemoteSnapshotVersion]; } - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { return self.persistence.run("Apply remote event", [&]() -> FSTMaybeDocumentDictionary * { + // TODO(gsoltis): move the sequence number into the reference delegate. + FSTListenSequenceNumber sequenceNumber = [self.listenSequence next]; id<FSTQueryCache> queryCache = self.queryCache; [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( @@ -274,6 +278,18 @@ NS_ASSUME_NONNULL_BEGIN return; } + // Update the resume token if the change includes one. Don't clear any preexisting value. + // Bump the sequence number as well, so that documents being removed now are ordered later + // than documents that were previously removed from this target. + NSData *resumeToken = change.resumeToken; + if (resumeToken.length > 0) { + queryData = [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion + resumeToken:resumeToken + sequenceNumber:sequenceNumber]; + self.targetIDs[targetIDNumber] = queryData; + [self.queryCache updateQueryData:queryData]; + } + FSTTargetMapping *mapping = change.mapping; if (mapping) { // First make sure that all references are deleted. @@ -291,61 +307,58 @@ NS_ASSUME_NONNULL_BEGIN 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 updateQueryData:queryData]; - } }]; // TODO(klimt): This could probably be an NSMutableDictionary. - FSTDocumentKeySet *changedDocKeys = [FSTDocumentKeySet keySet]; + DocumentKeySet changedDocKeys; + const DocumentKeySet &limboDocuments = remoteEvent.limboDocumentChanges; for (const auto &kv : remoteEvent.documentUpdates) { const DocumentKey &key = kv.first; FSTMaybeDocument *doc = kv.second; - changedDocKeys = [changedDocKeys setByAddingObject:key]; + changedDocKeys = changedDocKeys.insert(key); FSTMaybeDocument *existingDoc = [self.remoteDocumentCache 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 + // make an exception for SnapshotVersion::None() 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) { + if (!existingDoc || SnapshotVersion{doc.version} == SnapshotVersion::None() || + SnapshotVersion{doc.version} >= SnapshotVersion{existingDoc.version}) { [self.remoteDocumentCache addEntry:doc]; } else { FSTLog( @"FSTLocalStore Ignoring outdated watch update for %s. " - "Current version: %@ Watch version: %@", - key.ToString().c_str(), existingDoc.version, doc.version); + "Current version: %s Watch version: %s", + key.ToString().c_str(), existingDoc.version.timestamp().ToString().c_str(), + doc.version.timestamp().ToString().c_str()); } // 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]; + if (limboDocuments.contains(key)) { + [self.persistence.referenceDelegate limboDocumentUpdated: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); + const SnapshotVersion &lastRemoteVersion = [self.queryCache lastRemoteSnapshotVersion]; + const SnapshotVersion &remoteVersion = remoteEvent.snapshotVersion; + if (remoteVersion != SnapshotVersion::None()) { + FSTAssert(remoteVersion >= lastRemoteVersion, + @"Watch stream reverted to previous snapshot?? (%s < %s)", + remoteVersion.timestamp().ToString().c_str(), + lastRemoteVersion.timestamp().ToString().c_str()); [self.queryCache setLastRemoteSnapshotVersion:remoteVersion]; } - FSTDocumentKeySet *releasedWriteKeys = [self releaseHeldBatchResults]; + DocumentKeySet releasedWriteKeys = [self releaseHeldBatchResults]; // Union the two key sets. - __block FSTDocumentKeySet *keysToRecalc = changedDocKeys; - [releasedWriteKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - keysToRecalc = [keysToRecalc setByAddingObject:key]; - }]; + DocumentKeySet keysToRecalc = changedDocKeys; + for (const DocumentKey &key : releasedWriteKeys) { + keysToRecalc = keysToRecalc.insert(key); + } return [self.localDocuments documentsForKeys:keysToRecalc]; }); @@ -358,6 +371,9 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryData *queryData = [self.queryCache queryDataForQuery:view.query]; FSTAssert(queryData, @"Local view changes contain unallocated query."); FSTTargetID targetID = queryData.targetID; + for (const DocumentKey &key : view.removedKeys) { + [self->_persistence.referenceDelegate removeReference:key target:targetID]; + } [localViewReferences addReferencesToKeys:view.addedKeys forID:targetID]; [localViewReferences removeReferencesToKeys:view.removedKeys forID:targetID]; } @@ -408,6 +424,7 @@ NS_ASSUME_NONNULL_BEGIN if (self.garbageCollector.isEager) { [self.queryCache removeQueryData:queryData]; } + [self.persistence.referenceDelegate removeTarget:queryData]; [self.targetIDs removeObjectForKey:@(queryData.targetID)]; // If this was the last watch target, then we won't get any more watch snapshots, so we should @@ -424,8 +441,8 @@ NS_ASSUME_NONNULL_BEGIN }); } -- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID { - return self.persistence.run("RemoteDocumentKeysForTarget", [&]() -> FSTDocumentKeySet * { +- (DocumentKeySet)remoteDocumentKeysForTarget:(FSTTargetID)targetID { + return self.persistence.run("RemoteDocumentKeysForTarget", [&]() -> DocumentKeySet { return [self.queryCache matchingKeysForTargetID:targetID]; }); } @@ -449,7 +466,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return the set of keys of docs that were modified by those writes. */ -- (FSTDocumentKeySet *)releaseHeldBatchResults { +- (DocumentKeySet)releaseHeldBatchResults { NSMutableArray<FSTMutationBatchResult *> *toRelease = [NSMutableArray array]; for (FSTMutationBatchResult *batchResult in self.heldBatchResults) { if (![self isRemoteUpToVersion:batchResult.commitVersion]) { @@ -459,25 +476,24 @@ NS_ASSUME_NONNULL_BEGIN } if (toRelease.count == 0) { - return [FSTDocumentKeySet keySet]; + return DocumentKeySet{}; } else { [self.heldBatchResults removeObjectsInRange:NSMakeRange(0, toRelease.count)]; return [self releaseBatchResults:toRelease]; } } -- (BOOL)isRemoteUpToVersion:(FSTSnapshotVersion *)version { +- (BOOL)isRemoteUpToVersion:(const SnapshotVersion &)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; + return version <= self.queryCache.lastRemoteSnapshotVersion || self.targetIDs.count == 0; } -- (BOOL)shouldHoldBatchResultWithVersion:(FSTSnapshotVersion *)version { +- (BOOL)shouldHoldBatchResultWithVersion:(const SnapshotVersion &)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 { +- (DocumentKeySet)releaseBatchResults:(NSArray<FSTMutationBatchResult *> *)batchResults { NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array]; for (FSTMutationBatchResult *batchResult in batchResults) { [self applyBatchResult:batchResult]; @@ -487,36 +503,37 @@ NS_ASSUME_NONNULL_BEGIN return [self removeMutationBatches:batches]; } -- (FSTDocumentKeySet *)removeMutationBatch:(FSTMutationBatch *)batch { +- (DocumentKeySet)removeMutationBatch:(FSTMutationBatch *)batch { return [self removeMutationBatches:@[ batch ]]; } /** Removes all the mutation batches named in the given array. */ -- (FSTDocumentKeySet *)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches { - // TODO(klimt): Could this be an NSMutableDictionary? - __block FSTDocumentKeySet *affectedDocs = [FSTDocumentKeySet keySet]; - +- (DocumentKeySet)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches { + DocumentKeySet affectedDocs; for (FSTMutationBatch *batch in batches) { for (FSTMutation *mutation in batch.mutations) { const DocumentKey &key = mutation.key; - affectedDocs = [affectedDocs setByAddingObject:key]; + affectedDocs = affectedDocs.insert(key); } } [self.mutationQueue removeMutationBatches:batches]; - return affectedDocs; } - (void)applyBatchResult:(FSTMutationBatchResult *)batchResult { FSTMutationBatch *batch = batchResult.batch; - FSTDocumentKeySet *docKeys = batch.keys; - [docKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *docKey, BOOL *stop) { + DocumentKeySet docKeys = batch.keys; + const DocumentVersionMap &versions = batchResult.docVersions; + for (const DocumentKey &docKey : docKeys) { FSTMaybeDocument *_Nullable remoteDoc = [self.remoteDocumentCache 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) { + + auto ackVersionIter = versions.find(docKey); + FSTAssert(ackVersionIter != versions.end(), + @"docVersions should contain every doc in the write."); + const SnapshotVersion &ackVersion = ackVersionIter->second; + if (!doc || doc.version < ackVersion) { doc = [batch applyTo:doc documentKey:docKey mutationBatchResult:batchResult]; if (!doc) { FSTAssert(!remoteDoc, @"Mutation batch %@ applied to document %@ resulted in nil.", batch, @@ -525,7 +542,7 @@ NS_ASSUME_NONNULL_BEGIN [self.remoteDocumentCache addEntry:doc]; } } - }]; + } } @end diff --git a/Firestore/Source/Local/FSTLocalViewChanges.h b/Firestore/Source/Local/FSTLocalViewChanges.h index eb84642..143d010 100644 --- a/Firestore/Source/Local/FSTLocalViewChanges.h +++ b/Firestore/Source/Local/FSTLocalViewChanges.h @@ -16,7 +16,7 @@ #import <Foundation/Foundation.h> -#import "Firestore/Source/Model/FSTDocumentKeySet.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @class FSTDocumentSet; @class FSTMutation; @@ -34,16 +34,17 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTLocalViewChanges : NSObject + (instancetype)changesForQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys; + addedKeys:(firebase::firestore::model::DocumentKeySet)addedKeys + removedKeys:(firebase::firestore::model::DocumentKeySet)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; + +- (const firebase::firestore::model::DocumentKeySet &)addedKeys; +- (const firebase::firestore::model::DocumentKeySet &)removedKeys; @end diff --git a/Firestore/Source/Local/FSTLocalViewChanges.mm b/Firestore/Source/Local/FSTLocalViewChanges.mm index 9a7f445..eb6b259 100644 --- a/Firestore/Source/Local/FSTLocalViewChanges.mm +++ b/Firestore/Source/Local/FSTLocalViewChanges.mm @@ -16,31 +16,38 @@ #import "Firestore/Source/Local/FSTLocalViewChanges.h" +#include <utility> + #import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Model/FSTDocument.h" +using firebase::firestore::model::DocumentKeySet; + NS_ASSUME_NONNULL_BEGIN @interface FSTLocalViewChanges () - (instancetype)initWithQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys NS_DESIGNATED_INITIALIZER; + addedKeys:(DocumentKeySet)addedKeys + removedKeys:(DocumentKeySet)removedKeys NS_DESIGNATED_INITIALIZER; @end -@implementation FSTLocalViewChanges +@implementation FSTLocalViewChanges { + DocumentKeySet _addedKeys; + DocumentKeySet _removedKeys; +} + (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot { - FSTDocumentKeySet *addedKeys = [FSTDocumentKeySet keySet]; - FSTDocumentKeySet *removedKeys = [FSTDocumentKeySet keySet]; + DocumentKeySet addedKeys; + DocumentKeySet removedKeys; for (FSTDocumentViewChange *docChange in viewSnapshot.documentChanges) { switch (docChange.type) { case FSTDocumentViewChangeTypeAdded: - addedKeys = [addedKeys setByAddingObject:docChange.document.key]; + addedKeys = addedKeys.insert(docChange.document.key); break; case FSTDocumentViewChangeTypeRemoved: - removedKeys = [removedKeys setByAddingObject:docChange.document.key]; + removedKeys = removedKeys.insert(docChange.document.key); break; default: @@ -49,28 +56,39 @@ NS_ASSUME_NONNULL_BEGIN } } - return [self changesForQuery:viewSnapshot.query addedKeys:addedKeys removedKeys:removedKeys]; + return [self changesForQuery:viewSnapshot.query + addedKeys:std::move(addedKeys) + removedKeys:std::move(removedKeys)]; } + (instancetype)changesForQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys { - return - [[FSTLocalViewChanges alloc] initWithQuery:query addedKeys:addedKeys removedKeys:removedKeys]; + addedKeys:(DocumentKeySet)addedKeys + removedKeys:(DocumentKeySet)removedKeys { + return [[FSTLocalViewChanges alloc] initWithQuery:query + addedKeys:std::move(addedKeys) + removedKeys:std::move(removedKeys)]; } - (instancetype)initWithQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys { + addedKeys:(DocumentKeySet)addedKeys + removedKeys:(DocumentKeySet)removedKeys { self = [super init]; if (self) { _query = query; - _addedKeys = addedKeys; - _removedKeys = removedKeys; + _addedKeys = std::move(addedKeys); + _removedKeys = std::move(removedKeys); } return self; } +- (const DocumentKeySet &)addedKeys { + return _addedKeys; +} + +- (const DocumentKeySet &)removedKeys { + return _removedKeys; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.h b/Firestore/Source/Local/FSTMemoryMutationQueue.h index f0786cc..fd46a6e 100644 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.h +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.h @@ -16,6 +16,7 @@ #import <Foundation/Foundation.h> +#import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Local/FSTMutationQueue.h" @protocol FSTGarbageCollector; @@ -24,7 +25,9 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTMemoryMutationQueue : NSObject <FSTMutationQueue> -+ (instancetype)mutationQueue; +- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; /** The garbage collector to notify about potential garbage keys. */ @property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector; diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.mm b/Firestore/Source/Local/FSTMemoryMutationQueue.mm index 8028bb3..e05ee64 100644 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.mm +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.mm @@ -18,9 +18,11 @@ #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTDocumentReference.h" +#import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/third_party/Immutable/FSTImmutableSortedSet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" @@ -72,14 +74,13 @@ static const NSComparator NumberComparator = ^NSComparisonResult(NSNumber *left, @end -@implementation FSTMemoryMutationQueue - -+ (instancetype)mutationQueue { - return [[FSTMemoryMutationQueue alloc] init]; +@implementation FSTMemoryMutationQueue { + FSTMemoryPersistence *_persistence; } -- (instancetype)init { +- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence { if (self = [super init]) { + _persistence = persistence; _queue = [NSMutableArray array]; _batchesByDocumentKey = [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; @@ -347,6 +348,7 @@ static const NSComparator NumberComparator = ^NSComparisonResult(NSNumber *left, for (FSTMutation *mutation in batch.mutations) { const DocumentKey &key = mutation.key; [garbageCollector addPotentialGarbageKey:key]; + [_persistence.referenceDelegate removeMutationReference:key]; FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:batchID]; references = [references setByRemovingObject:reference]; diff --git a/Firestore/Source/Local/FSTMemoryPersistence.mm b/Firestore/Source/Local/FSTMemoryPersistence.mm index 8d74881..3466f3e 100644 --- a/Firestore/Source/Local/FSTMemoryPersistence.mm +++ b/Firestore/Source/Local/FSTMemoryPersistence.mm @@ -59,7 +59,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init { if (self = [super init]) { - _queryCache = [[FSTMemoryQueryCache alloc] init]; + _queryCache = [[FSTMemoryQueryCache alloc] initWithPersistence:self]; _remoteDocumentCache = [[FSTMemoryRemoteDocumentCache alloc] init]; } return self; @@ -78,6 +78,10 @@ NS_ASSUME_NONNULL_BEGIN self.started = NO; } +- (_Nullable id<FSTReferenceDelegate>)referenceDelegate { + return nil; +} + - (const FSTTransactionRunner &)run { return _transactionRunner; } @@ -85,7 +89,7 @@ NS_ASSUME_NONNULL_BEGIN - (id<FSTMutationQueue>)mutationQueueForUser:(const User &)user { id<FSTMutationQueue> queue = _mutationQueues[user]; if (!queue) { - queue = [FSTMemoryMutationQueue mutationQueue]; + queue = [[FSTMemoryMutationQueue alloc] initWithPersistence:self]; _mutationQueues[user] = queue; } return queue; diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.h b/Firestore/Source/Local/FSTMemoryQueryCache.h index 98f0277..126ce59 100644 --- a/Firestore/Source/Local/FSTMemoryQueryCache.h +++ b/Firestore/Source/Local/FSTMemoryQueryCache.h @@ -20,11 +20,18 @@ NS_ASSUME_NONNULL_BEGIN +@class FSTMemoryPersistence; + /** * An implementation of the FSTQueryCache protocol that merely keeps queries in memory, suitable * for online only clients with persistence disabled. */ @interface FSTMemoryQueryCache : NSObject <FSTQueryCache> + +- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.mm b/Firestore/Source/Local/FSTMemoryQueryCache.mm index 18d14f2..2eba4f6 100644 --- a/Firestore/Source/Local/FSTMemoryQueryCache.mm +++ b/Firestore/Source/Local/FSTMemoryQueryCache.mm @@ -16,12 +16,19 @@ #import "Firestore/Source/Local/FSTMemoryQueryCache.h" +#include <utility> + #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Local/FSTReferenceSet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentKey; NS_ASSUME_NONNULL_BEGIN @@ -38,18 +45,20 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) FSTListenSequenceNumber highestListenSequenceNumber; -/** The last received snapshot version. */ -@property(nonatomic, strong) FSTSnapshotVersion *lastRemoteSnapshotVersion; - @end -@implementation FSTMemoryQueryCache +@implementation FSTMemoryQueryCache { + FSTMemoryPersistence *_persistence; + /** The last received snapshot version. */ + SnapshotVersion _lastRemoteSnapshotVersion; +} -- (instancetype)init { +- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence { if (self = [super init]) { + _persistence = persistence; _queries = [NSMutableDictionary dictionary]; _references = [[FSTReferenceSet alloc] init]; - _lastRemoteSnapshotVersion = [FSTSnapshotVersion noVersion]; + _lastRemoteSnapshotVersion = SnapshotVersion::None(); } return self; } @@ -69,14 +78,13 @@ NS_ASSUME_NONNULL_BEGIN return _highestListenSequenceNumber; } -/*- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { +- (const SnapshotVersion &)lastRemoteSnapshotVersion { return _lastRemoteSnapshotVersion; } -- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - group:(FSTWriteGroup *)group { - _lastRemoteSnapshotVersion = snapshotVersion; -}*/ +- (void)setLastRemoteSnapshotVersion:(SnapshotVersion)snapshotVersion { + _lastRemoteSnapshotVersion = std::move(snapshotVersion); +} - (void)addQueryData:(FSTQueryData *)queryData { self.queries[queryData.query] = queryData; @@ -113,19 +121,25 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark Reference tracking -- (void)addMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID { +- (void)addMatchingKeys:(const DocumentKeySet &)keys forTargetID:(FSTTargetID)targetID { [self.references addReferencesToKeys:keys forID:targetID]; + for (const DocumentKey &key : keys) { + [_persistence.referenceDelegate addReference:key target:targetID]; + } } -- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID { +- (void)removeMatchingKeys:(const DocumentKeySet &)keys forTargetID:(FSTTargetID)targetID { [self.references removeReferencesToKeys:keys forID:targetID]; + for (const DocumentKey &key : keys) { + [_persistence.referenceDelegate removeReference:key target:targetID]; + } } - (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID { [self.references removeReferencesForID:targetID]; } -- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { +- (DocumentKeySet)matchingKeysForTargetID:(FSTTargetID)targetID { return [self.references referencedKeysForID:targetID]; } diff --git a/Firestore/Source/Local/FSTPersistence.h b/Firestore/Source/Local/FSTPersistence.h index 2294ef1..417ff3f 100644 --- a/Firestore/Source/Local/FSTPersistence.h +++ b/Firestore/Source/Local/FSTPersistence.h @@ -16,12 +16,16 @@ #import <Foundation/Foundation.h> +#import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Util/FSTAssert.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +@class FSTDocumentKey; @protocol FSTMutationQueue; @protocol FSTQueryCache; +@class FSTQueryData; @protocol FSTRemoteDocumentCache; +@class FSTReferenceSet; NS_ASSUME_NONNULL_BEGIN @@ -56,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN * of its reads and writes in order to avoid relying on being able to read back uncommitted writes. */ struct FSTTransactionRunner; +@protocol FSTReferenceDelegate; @protocol FSTPersistence <NSObject> /** @@ -87,6 +92,12 @@ struct FSTTransactionRunner; @property(nonatomic, readonly, assign) const FSTTransactionRunner &run; +/** + * This property provides access to hooks around the document reference lifecycle. It is initially + * nullable while being implemented, but the goal is to eventually have it be non-nil. + */ +@property(nonatomic, readonly, strong) _Nullable id<FSTReferenceDelegate> referenceDelegate; + @end @protocol FSTTransactional @@ -97,6 +108,52 @@ struct FSTTransactionRunner; @end +/** + * An FSTReferenceDelegate instance handles all of the hooks into the document-reference lifecycle. + * This includes being added to a target, being removed from a target, being subject to mutation, + * and being mutated by the user. + * + * Different implementations may do different things with each of these events. Not every + * implementation needs to do something with every lifecycle hook. + * + * Implementations that care about sequence numbers are responsible for generating them and making + * them available. + */ +@protocol FSTReferenceDelegate + +/** + * Registers an FSTReferenceSet of documents that should be considered 'referenced' and not eligible + * for removal during garbage collection. + */ +- (void)addInMemoryPins:(FSTReferenceSet *)set; + +/** + * Notify the delegate that a target was removed. + */ +- (void)removeTarget:(FSTQueryData *)queryData; + +/** + * Notify the delegate that the given document was added to the given target. + */ +- (void)addReference:(FSTDocumentKey *)key target:(FSTTargetID)targetID; + +/** + * Notify the delegate that the given document was removed from the given target. + */ +- (void)removeReference:(FSTDocumentKey *)key target:(FSTTargetID)targetID; + +/** + * Notify the delegate that a document is no longer being mutated by the user. + */ +- (void)removeMutationReference:(FSTDocumentKey *)key; + +/** + * Notify the delegate that a limbo document was updated. + */ +- (void)limboDocumentUpdated:(FSTDocumentKey *)key; + +@end + struct FSTTransactionRunner { // Intentionally disable nullability checking for this function. We cannot properly annotate // the function because this function can handle both pointer and non-pointer types. It is an error diff --git a/Firestore/Source/Local/FSTQueryCache.h b/Firestore/Source/Local/FSTQueryCache.h index d797d49..1ad46aa 100644 --- a/Firestore/Source/Local/FSTQueryCache.h +++ b/Firestore/Source/Local/FSTQueryCache.h @@ -18,13 +18,14 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Local/FSTGarbageCollector.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" + +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTDocumentSet; @class FSTMaybeDocument; @class FSTQuery; @class FSTQueryData; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -61,7 +62,7 @@ NS_ASSUME_NONNULL_BEGIN * * This is updated whenever our we get a TargetChange with a read_time and empty target_ids. */ -- (FSTSnapshotVersion *)lastRemoteSnapshotVersion; +- (const firebase::firestore::model::SnapshotVersion &)lastRemoteSnapshotVersion; /** * Set the snapshot version representing the last consistent snapshot received from the backend. @@ -69,7 +70,7 @@ NS_ASSUME_NONNULL_BEGIN * * @param snapshotVersion The new snapshot version. */ -- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion; +- (void)setLastRemoteSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion; /** * Adds an entry in the cache. @@ -104,15 +105,17 @@ NS_ASSUME_NONNULL_BEGIN - (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; +- (void)addMatchingKeys:(const firebase::firestore::model::DocumentKeySet &)keys + forTargetID:(FSTTargetID)targetID; /** Removes the given document keys from the cached query results of the given target ID. */ -- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID; +- (void)removeMatchingKeys:(const firebase::firestore::model::DocumentKeySet &)keys + forTargetID:(FSTTargetID)targetID; /** Removes all the keys in the query results of the given target ID. */ - (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID; -- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID; +- (firebase::firestore::model::DocumentKeySet)matchingKeysForTargetID:(FSTTargetID)targetID; @end diff --git a/Firestore/Source/Local/FSTQueryData.h b/Firestore/Source/Local/FSTQueryData.h index 5db2de6..bde0a15 100644 --- a/Firestore/Source/Local/FSTQueryData.h +++ b/Firestore/Source/Local/FSTQueryData.h @@ -18,8 +18,9 @@ #import "Firestore/Source/Core/FSTTypes.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + @class FSTQuery; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -42,7 +43,7 @@ typedef NS_ENUM(NSInteger, FSTQueryPurpose) { targetID:(FSTTargetID)targetID listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber purpose:(FSTQueryPurpose)purpose - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion resumeToken:(NSData *)resumeToken NS_DESIGNATED_INITIALIZER; /** Convenience initializer for use when creating an FSTQueryData for the first time. */ @@ -53,9 +54,17 @@ typedef NS_ENUM(NSInteger, FSTQueryPurpose) { - (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; +/** + * Creates a new query data instance with an updated snapshot version, resume token, and sequence + * number. + */ +- (instancetype)queryDataByReplacingSnapshotVersion: + (firebase::firestore::model::SnapshotVersion)snapshotVersion + resumeToken:(NSData *)resumeToken + sequenceNumber:(FSTListenSequenceNumber)sequenceNumber; + +/** The latest snapshot version seen for this target. */ +- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion; /** The query being listened to. */ @property(nonatomic, strong, readonly) FSTQuery *query; @@ -71,9 +80,6 @@ typedef NS_ENUM(NSInteger, FSTQueryPurpose) { /** 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 diff --git a/Firestore/Source/Local/FSTQueryData.mm b/Firestore/Source/Local/FSTQueryData.mm index 6bb716a..5087dfc 100644 --- a/Firestore/Source/Local/FSTQueryData.mm +++ b/Firestore/Source/Local/FSTQueryData.mm @@ -16,18 +16,25 @@ #import "Firestore/Source/Local/FSTQueryData.h" +#include <utility> + #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" + +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN -@implementation FSTQueryData +@implementation FSTQueryData { + SnapshotVersion _snapshotVersion; +} - (instancetype)initWithQuery:(FSTQuery *)query targetID:(FSTTargetID)targetID listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber purpose:(FSTQueryPurpose)purpose - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion:(SnapshotVersion)snapshotVersion resumeToken:(NSData *)resumeToken { self = [super init]; if (self) { @@ -35,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN _targetID = targetID; _sequenceNumber = sequenceNumber; _purpose = purpose; - _snapshotVersion = snapshotVersion; + _snapshotVersion = std::move(snapshotVersion); _resumeToken = [resumeToken copy]; } return self; @@ -49,10 +56,14 @@ NS_ASSUME_NONNULL_BEGIN targetID:targetID listenSequenceNumber:sequenceNumber purpose:purpose - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() resumeToken:[NSData data]]; } +- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion { + return _snapshotVersion; +} + - (BOOL)isEqual:(id)object { if (self == object) { return YES; @@ -63,7 +74,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryData *other = (FSTQueryData *)object; return [self.query isEqual:other.query] && self.targetID == other.targetID && - self.purpose == other.purpose && [self.snapshotVersion isEqual:other.snapshotVersion] && + self.purpose == other.purpose && self.snapshotVersion == other.snapshotVersion && [self.resumeToken isEqual:other.resumeToken]; } @@ -78,18 +89,19 @@ NS_ASSUME_NONNULL_BEGIN - (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]; + stringWithFormat:@"<FSTQueryData: query:%@ target:%d purpose:%lu version:%s resumeToken:%@)>", + self.query, self.targetID, (unsigned long)self.purpose, + self.snapshotVersion.timestamp().ToString().c_str(), self.resumeToken]; } -- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - resumeToken:(NSData *)resumeToken { +- (instancetype)queryDataByReplacingSnapshotVersion:(SnapshotVersion)snapshotVersion + resumeToken:(NSData *)resumeToken + sequenceNumber:(FSTListenSequenceNumber)sequenceNumber { return [[FSTQueryData alloc] initWithQuery:self.query targetID:self.targetID - listenSequenceNumber:self.sequenceNumber + listenSequenceNumber:sequenceNumber purpose:self.purpose - snapshotVersion:snapshotVersion + snapshotVersion:std::move(snapshotVersion) resumeToken:resumeToken]; } diff --git a/Firestore/Source/Local/FSTReferenceSet.h b/Firestore/Source/Local/FSTReferenceSet.h index 9d842cb..9a90a40 100644 --- a/Firestore/Source/Local/FSTReferenceSet.h +++ b/Firestore/Source/Local/FSTReferenceSet.h @@ -18,7 +18,8 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Local/FSTGarbageCollector.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" + +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" NS_ASSUME_NONNULL_BEGIN @@ -47,13 +48,14 @@ NS_ASSUME_NONNULL_BEGIN - (void)addReferenceToKey:(const firebase::firestore::model::DocumentKey &)key forID:(int)ID; /** Add references to the given document keys for the given ID. */ -- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID; +- (void)addReferencesToKeys:(const firebase::firestore::model::DocumentKeySet &)keys forID:(int)ID; /** Removes a reference to the given document key for the given ID. */ - (void)removeReferenceToKey:(const firebase::firestore::model::DocumentKey &)key forID:(int)ID; /** Removes references to the given document keys for the given ID. */ -- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID; +- (void)removeReferencesToKeys:(const firebase::firestore::model::DocumentKeySet &)keys + forID:(int)ID; /** Clears all references with a given ID. Calls -removeReferenceToKey: for each key removed. */ - (void)removeReferencesForID:(int)ID; @@ -62,7 +64,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)removeAllReferences; /** Returns all of the document keys that have had references added for the given ID. */ -- (FSTDocumentKeySet *)referencedKeysForID:(int)ID; +- (firebase::firestore::model::DocumentKeySet)referencedKeysForID:(int)ID; @end diff --git a/Firestore/Source/Local/FSTReferenceSet.mm b/Firestore/Source/Local/FSTReferenceSet.mm index 14f5d47..6b34725 100644 --- a/Firestore/Source/Local/FSTReferenceSet.mm +++ b/Firestore/Source/Local/FSTReferenceSet.mm @@ -17,10 +17,12 @@ #import "Firestore/Source/Local/FSTReferenceSet.h" #import "Firestore/Source/Local/FSTDocumentReference.h" +#import "Firestore/third_party/Immutable/FSTImmutableSortedSet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -68,20 +70,20 @@ NS_ASSUME_NONNULL_BEGIN self.referencesByID = [self.referencesByID setByAddingObject:reference]; } -- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { - [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { +- (void)addReferencesToKeys:(const DocumentKeySet &)keys forID:(int)ID { + for (const DocumentKey &key : keys) { [self addReferenceToKey:key forID:ID]; - }]; + } } - (void)removeReferenceToKey:(const DocumentKey &)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) { +- (void)removeReferencesToKeys:(const DocumentKeySet &)keys forID:(int)ID { + for (const DocumentKey &key : keys) { [self removeReferenceToKey:key forID:ID]; - }]; + } } - (void)removeReferencesForID:(int)ID { @@ -109,17 +111,17 @@ NS_ASSUME_NONNULL_BEGIN [self.garbageCollector addPotentialGarbageKey:reference.key]; } -- (FSTDocumentKeySet *)referencedKeysForID:(int)ID { +- (DocumentKeySet)referencedKeysForID:(int)ID { FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:DocumentKey::Empty() ID:ID]; FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:DocumentKey::Empty() ID:(ID + 1)]; - __block FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + __block DocumentKeySet keys; [self.referencesByID enumerateObjectsFrom:start to:end usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { - keys = [keys setByAddingObject:reference.key]; + keys = keys.insert(reference.key); }]; return keys; } diff --git a/Firestore/Source/Model/FSTDocument.h b/Firestore/Source/Model/FSTDocument.h index 47e4d28..0f8d4b3 100644 --- a/Firestore/Source/Model/FSTDocument.h +++ b/Firestore/Source/Model/FSTDocument.h @@ -18,10 +18,10 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTFieldValue; @class FSTObjectValue; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -32,14 +32,13 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTMaybeDocument : NSObject <NSCopying> - (id)init __attribute__((unavailable("Abstract base class"))); - (const firebase::firestore::model::DocumentKey &)key; - -@property(nonatomic, readonly) FSTSnapshotVersion *version; +- (const firebase::firestore::model::SnapshotVersion &)version; @end @interface FSTDocument : FSTMaybeDocument + (instancetype)documentWithData:(FSTObjectValue *)data key:(firebase::firestore::model::DocumentKey)key - version:(FSTSnapshotVersion *)version + version:(firebase::firestore::model::SnapshotVersion)version hasLocalMutations:(BOOL)mutations; - (nullable FSTFieldValue *)fieldForPath:(const firebase::firestore::model::FieldPath &)path; @@ -51,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTDeletedDocument : FSTMaybeDocument + (instancetype)documentWithKey:(firebase::firestore::model::DocumentKey)key - version:(FSTSnapshotVersion *)version; + version:(firebase::firestore::model::SnapshotVersion)version; @end /** An NSComparator suitable for comparing docs using only their keys. */ diff --git a/Firestore/Source/Model/FSTDocument.mm b/Firestore/Source/Model/FSTDocument.mm index 9898c2a..8ebc9ab 100644 --- a/Firestore/Source/Model/FSTDocument.mm +++ b/Firestore/Source/Model/FSTDocument.mm @@ -18,7 +18,6 @@ #include <utility> -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Util/FSTAssert.h" @@ -29,26 +28,28 @@ namespace util = firebase::firestore::util; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldPath; +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN @interface FSTMaybeDocument () - (instancetype)initWithKey:(DocumentKey)key - version:(FSTSnapshotVersion *)version NS_DESIGNATED_INITIALIZER; + version:(SnapshotVersion)version NS_DESIGNATED_INITIALIZER; @end @implementation FSTMaybeDocument { DocumentKey _key; + SnapshotVersion _version; } -- (instancetype)initWithKey:(DocumentKey)key version:(FSTSnapshotVersion *)version { +- (instancetype)initWithKey:(DocumentKey)key version:(SnapshotVersion)version { FSTAssert(!!version, @"Version must not be nil."); self = [super init]; if (self) { _key = std::move(key); - _version = version; + _version = std::move(version); } return self; } @@ -62,25 +63,29 @@ NS_ASSUME_NONNULL_BEGIN return _key; } +- (const SnapshotVersion &)version { + return _version; +} + @end @implementation FSTDocument + (instancetype)documentWithData:(FSTObjectValue *)data key:(DocumentKey)key - version:(FSTSnapshotVersion *)version + version:(SnapshotVersion)version hasLocalMutations:(BOOL)mutations { return [[FSTDocument alloc] initWithData:data key:std::move(key) - version:version + version:std::move(version) hasLocalMutations:mutations]; } - (instancetype)initWithData:(FSTObjectValue *)data key:(DocumentKey)key - version:(FSTSnapshotVersion *)version + version:(SnapshotVersion)version hasLocalMutations:(BOOL)mutations { - self = [super initWithKey:std::move(key) version:version]; + self = [super initWithKey:std::move(key) version:std::move(version)]; if (self) { _data = data; _localMutations = mutations; @@ -110,8 +115,9 @@ NS_ASSUME_NONNULL_BEGIN } - (NSString *)description { - return [NSString stringWithFormat:@"<FSTDocument: key:%s version:%@ localMutations:%@ data:%@>", - self.key.ToString().c_str(), self.version, + return [NSString stringWithFormat:@"<FSTDocument: key:%s version:%s localMutations:%@ data:%@>", + self.key.ToString().c_str(), + self.version.timestamp().ToString().c_str(), self.localMutations ? @"YES" : @"NO", self.data]; } @@ -123,8 +129,8 @@ NS_ASSUME_NONNULL_BEGIN @implementation FSTDeletedDocument -+ (instancetype)documentWithKey:(DocumentKey)key version:(FSTSnapshotVersion *)version { - return [[FSTDeletedDocument alloc] initWithKey:std::move(key) version:version]; ++ (instancetype)documentWithKey:(DocumentKey)key version:(SnapshotVersion)version { + return [[FSTDeletedDocument alloc] initWithKey:std::move(key) version:std::move(version)]; } - (BOOL)isEqual:(id)other { diff --git a/Firestore/Source/Model/FSTDocumentKey.mm b/Firestore/Source/Model/FSTDocumentKey.mm index 679d7a6..d29df86 100644 --- a/Firestore/Source/Model/FSTDocumentKey.mm +++ b/Firestore/Source/Model/FSTDocumentKey.mm @@ -23,6 +23,7 @@ #import "Firestore/Source/Util/FSTAssert.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; @@ -72,7 +73,7 @@ NS_ASSUME_NONNULL_BEGIN } - (NSUInteger)hash { - return _path.Hash(); + return util::Hash(_path); } - (NSString *)description { diff --git a/Firestore/Source/Model/FSTDocumentKeySet.mm b/Firestore/Source/Model/FSTDocumentKeySet.mm deleted file mode 100644 index f07b785..0000000 --- a/Firestore/Source/Model/FSTDocumentKeySet.mm +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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/Source/Model/FSTDocumentKeySet.h" - -#import "Firestore/Source/Model/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/FSTDocumentVersionDictionary.h b/Firestore/Source/Model/FSTDocumentVersionDictionary.h deleted file mode 100644 index 674614e..0000000 --- a/Firestore/Source/Model/FSTDocumentVersionDictionary.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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 "Firestore/third_party/Immutable/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.mm b/Firestore/Source/Model/FSTDocumentVersionDictionary.mm deleted file mode 100644 index 870e082..0000000 --- a/Firestore/Source/Model/FSTDocumentVersionDictionary.mm +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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/Source/Model/FSTDocumentVersionDictionary.h" - -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Model/FSTDocumentKey.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/FSTMutation.h b/Firestore/Source/Model/FSTMutation.h index 7261f30..0acec15 100644 --- a/Firestore/Source/Model/FSTMutation.h +++ b/Firestore/Source/Model/FSTMutation.h @@ -24,13 +24,15 @@ #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/field_transform.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" +#include "absl/types/optional.h" + @class FSTDocument; @class FSTFieldValue; @class FSTMaybeDocument; @class FSTObjectValue; -@class FSTSnapshotVersion; @class FIRTimestamp; NS_ASSUME_NONNULL_BEGIN @@ -40,12 +42,12 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTMutationResult : NSObject - (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version +- (instancetype)initWithVersion:(absl::optional<firebase::firestore::model::SnapshotVersion>)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; +- (const absl::optional<firebase::firestore::model::SnapshotVersion> &)version; /** * The resulting fields returned from the backend after a FSTTransformMutation has been committed. diff --git a/Firestore/Source/Model/FSTMutation.mm b/Firestore/Source/Model/FSTMutation.mm index 3432a7c..fdf6014 100644 --- a/Firestore/Source/Model/FSTMutation.mm +++ b/Firestore/Source/Model/FSTMutation.mm @@ -23,7 +23,6 @@ #import "FIRTimestamp.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Util/FSTAssert.h" @@ -36,6 +35,8 @@ #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" +#include "absl/types/optional.h" + using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldMask; @@ -43,23 +44,30 @@ using firebase::firestore::model::FieldPath; using firebase::firestore::model::FieldTransform; using firebase::firestore::model::Precondition; using firebase::firestore::model::ServerTimestampTransform; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TransformOperation; NS_ASSUME_NONNULL_BEGIN #pragma mark - FSTMutationResult -@implementation FSTMutationResult +@implementation FSTMutationResult { + absl::optional<SnapshotVersion> _version; +} -- (instancetype)initWithVersion:(nullable FSTSnapshotVersion *)version +- (instancetype)initWithVersion:(absl::optional<SnapshotVersion>)version transformResults:(nullable NSArray<FSTFieldValue *> *)transformResults { if (self = [super init]) { - _version = version; + _version = std::move(version); _transformResults = transformResults; } return self; } +- (const absl::optional<SnapshotVersion> &)version { + return _version; +} + @end #pragma mark - FSTMutation @@ -157,7 +165,7 @@ NS_ASSUME_NONNULL_BEGIN // If the document didn't exist before, create it. return [FSTDocument documentWithData:self.value key:self.key - version:[FSTSnapshotVersion noVersion] + version:SnapshotVersion::None() hasLocalMutations:hasLocalMutations]; } @@ -239,10 +247,10 @@ NS_ASSUME_NONNULL_BEGIN if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { // Precondition applied, so create the document if necessary const DocumentKey &key = maybeDoc ? maybeDoc.key : self.key; - FSTSnapshotVersion *version = maybeDoc ? maybeDoc.version : [FSTSnapshotVersion noVersion]; + SnapshotVersion version = maybeDoc ? maybeDoc.version : SnapshotVersion::None(); maybeDoc = [FSTDocument documentWithData:[FSTObjectValue objectValue] key:key - version:version + version:std::move(version) hasLocalMutations:hasLocalMutations]; } @@ -556,7 +564,7 @@ serverTransformResultsWithBaseDocument:(nullable FSTMaybeDocument *)baseDocument FSTAssert([maybeDoc.key isEqual:self.key], @"Can only delete a document with the same key"); } - return [FSTDeletedDocument documentWithKey:self.key version:[FSTSnapshotVersion noVersion]]; + return [FSTDeletedDocument documentWithKey:self.key version:SnapshotVersion::None()]; } @end diff --git a/Firestore/Source/Model/FSTMutationBatch.h b/Firestore/Source/Model/FSTMutationBatch.h index 3c82338..761a885 100644 --- a/Firestore/Source/Model/FSTMutationBatch.h +++ b/Firestore/Source/Model/FSTMutationBatch.h @@ -16,17 +16,29 @@ #import <Foundation/Foundation.h> +#include <unordered_map> + #import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" -#import "Firestore/Source/Model/FSTDocumentVersionDictionary.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTMutation; @class FIRTimestamp; @class FSTMutationResult; @class FSTMutationBatchResult; -@class FSTSnapshotVersion; + +namespace firebase { +namespace firestore { +namespace model { + +// TODO(wilhuff): make this type a member of MutationBatchResult once that's a C++ class. +using DocumentVersionMap = std::unordered_map<DocumentKey, SnapshotVersion, DocumentKeyHash>; + +} // namespace model +} // namespace firestore +} // namespace firebase NS_ASSUME_NONNULL_BEGIN @@ -85,7 +97,7 @@ extern const FSTBatchID kFSTBatchIDUnknown; - (FSTMutationBatch *)toTombstone; /** Returns the set of unique keys referenced by all mutations in the batch. */ -- (FSTDocumentKeySet *)keys; +- (firebase::firestore::model::DocumentKeySet)keys; @property(nonatomic, assign, readonly) FSTBatchID batchID; @property(nonatomic, strong, readonly) FIRTimestamp *localWriteTime; @@ -106,15 +118,17 @@ extern const FSTBatchID kFSTBatchIDUnknown; * (as docVersions). */ + (instancetype)resultWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion + commitVersion:(firebase::firestore::model::SnapshotVersion)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)mutationResults streamToken:(nullable NSData *)streamToken; +- (const firebase::firestore::model::SnapshotVersion &)commitVersion; + @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; + +- (const firebase::firestore::model::DocumentVersionMap &)docVersions; @end diff --git a/Firestore/Source/Model/FSTMutationBatch.mm b/Firestore/Source/Model/FSTMutationBatch.mm index e62a72c..1e9189c 100644 --- a/Firestore/Source/Model/FSTMutationBatch.mm +++ b/Firestore/Source/Model/FSTMutationBatch.mm @@ -16,16 +16,19 @@ #import "Firestore/Source/Model/FSTMutationBatch.h" +#include <utility> + #import "FIRTimestamp.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Util/FSTAssert.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" - using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeyHash; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentVersionMap; NS_ASSUME_NONNULL_BEGIN @@ -113,10 +116,10 @@ const FSTBatchID kFSTBatchIDUnknown = -1; } // TODO(klimt): This could use NSMutableDictionary instead. -- (FSTDocumentKeySet *)keys { - FSTDocumentKeySet *set = [FSTDocumentKeySet keySet]; +- (DocumentKeySet)keys { + DocumentKeySet set; for (FSTMutation *mutation in self.mutations) { - set = [set setByAddingObject:mutation.key]; + set = set.insert(mutation.key); } return set; } @@ -127,56 +130,66 @@ const FSTBatchID kFSTBatchIDUnknown = -1; @interface FSTMutationBatchResult () - (instancetype)initWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion + commitVersion:(SnapshotVersion)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)mutationResults streamToken:(nullable NSData *)streamToken - docVersions:(FSTDocumentVersionDictionary *)docVersions NS_DESIGNATED_INITIALIZER; + docVersions:(DocumentVersionMap)docVersions NS_DESIGNATED_INITIALIZER; @end -@implementation FSTMutationBatchResult +@implementation FSTMutationBatchResult { + SnapshotVersion _commitVersion; + DocumentVersionMap _docVersions; +} - (instancetype)initWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion + commitVersion:(SnapshotVersion)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)mutationResults streamToken:(nullable NSData *)streamToken - docVersions:(FSTDocumentVersionDictionary *)docVersions { + docVersions:(DocumentVersionMap)docVersions { if (self = [super init]) { _batch = batch; - _commitVersion = commitVersion; + _commitVersion = std::move(commitVersion); _mutationResults = mutationResults; _streamToken = streamToken; - _docVersions = docVersions; + _docVersions = std::move(docVersions); } return self; } +- (const SnapshotVersion &)commitVersion { + return _commitVersion; +} + +- (const DocumentVersionMap &)docVersions { + return _docVersions; +} + + (instancetype)resultWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion + commitVersion:(SnapshotVersion)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]; + DocumentVersionMap docVersions; NSArray<FSTMutation *> *mutations = batch.mutations; for (NSUInteger i = 0; i < mutations.count; i++) { - FSTSnapshotVersion *_Nullable version = mutationResults[i].version; + absl::optional<SnapshotVersion> 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]; + docVersions[mutations[i].key] = version.value(); } return [[FSTMutationBatchResult alloc] initWithBatch:batch - commitVersion:commitVersion + commitVersion:std::move(commitVersion) mutationResults:mutationResults streamToken:streamToken - docVersions:docVersions]; + docVersions:std::move(docVersions)]; } @end diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h index 4aa8c45..7baa30a 100644 --- a/Firestore/Source/Public/FIRDocumentReference.h +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -92,6 +92,23 @@ NS_SWIFT_NAME(DocumentReference) - (void)setData:(NSDictionary<NSString *, id> *)documentData merge:(BOOL)merge; /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData mergeFields:(NSArray<id> *)mergeFields; + +/** * Overwrites the document referred to by this `FIRDocumentReference`. If no document exists, it * is created. If a document already exists, it is overwritten. * @@ -121,6 +138,28 @@ NS_SWIFT_NAME(DocumentReference) completion:(nullable void (^)(NSError *_Nullable error))completion; /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + * @param completion A block to execute once the document has been successfully written to the + * server. This block will not be called while the client is offline, though local + * changes will be visible immediately. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData + mergeFields:(NSArray<id> *)mergeFields + 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). * diff --git a/Firestore/Source/Public/FIRTransaction.h b/Firestore/Source/Public/FIRTransaction.h index 2fa4430..e53414d 100644 --- a/Firestore/Source/Public/FIRTransaction.h +++ b/Firestore/Source/Public/FIRTransaction.h @@ -65,6 +65,30 @@ NS_SWIFT_NAME(Transaction) // clang-format on /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param data An `NSDictionary` containing the fields that make up the document + * to be written. + * @param document A reference to the document whose data should be overwritten. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields + NS_SWIFT_NAME(setData(_:forDocument:mergeFields:)); +// clang-format on + +/** * Updates fields in the document referred to by `document`. * If the document does not exist, the transaction will fail. * diff --git a/Firestore/Source/Public/FIRWriteBatch.h b/Firestore/Source/Public/FIRWriteBatch.h index 1568723..22d1b16 100644 --- a/Firestore/Source/Public/FIRWriteBatch.h +++ b/Firestore/Source/Public/FIRWriteBatch.h @@ -68,6 +68,29 @@ NS_SWIFT_NAME(WriteBatch) // clang-format on /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @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 mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields + NS_SWIFT_NAME(setData(_:forDocument:mergeFields:)); +// clang-format on + +/** * Updates fields in the document referred to by `document`. * If document does not exist, the write batch will fail. * diff --git a/Firestore/Source/Remote/FSTDatastore.h b/Firestore/Source/Remote/FSTDatastore.h index b3ba46c..da14b6e 100644 --- a/Firestore/Source/Remote/FSTDatastore.h +++ b/Firestore/Source/Remote/FSTDatastore.h @@ -31,7 +31,6 @@ @class FSTMutationResult; @class FSTQueryData; @class FSTSerializerBeta; -@class FSTSnapshotVersion; @class FSTWatchChange; @class FSTWatchStream; @class FSTWriteStream; diff --git a/Firestore/Source/Remote/FSTDatastore.mm b/Firestore/Source/Remote/FSTDatastore.mm index c7ee30f..f0852fe 100644 --- a/Firestore/Source/Remote/FSTDatastore.mm +++ b/Firestore/Source/Remote/FSTDatastore.mm @@ -185,7 +185,7 @@ typedef GRPCProtoCall * (^RPCFactory)(void); // 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]; + return [NSString stringWithFormat:@"gl-objc/ fire/%s grpc/", FIRFirestoreVersionString]; } /** Returns the string to be used as google-cloud-resource-prefix header value. */ diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h index 0f6b6c7..c84e34d 100644 --- a/Firestore/Source/Remote/FSTRemoteEvent.h +++ b/Firestore/Source/Remote/FSTRemoteEvent.h @@ -20,14 +20,14 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTDocument; @class FSTExistenceFilter; @class FSTMaybeDocument; -@class FSTSnapshotVersion; @class FSTWatchChange; @class FSTQueryData; @@ -43,6 +43,15 @@ NS_ASSUME_NONNULL_BEGIN * base class. */ @interface FSTTargetMapping : NSObject + +/** + * Strips out mapping changes that aren't actually changes. That is, if the document already + * existed in the target, and is being added in the target, and this is not a reset, we can + * skip doing the work to associate the document with the target because it has already been done. + */ +- (void)filterUpdatesUsingExistingKeys: + (const firebase::firestore::model::DocumentKeySet &)existingKeys; + @end #pragma mark - FSTResetMapping @@ -57,7 +66,7 @@ NS_ASSUME_NONNULL_BEGIN + (FSTResetMapping *)mappingWithDocuments:(NSArray<FSTDocument *> *)documents; /** The new set of documents for the target. */ -@property(nonatomic, strong, readonly) FSTDocumentKeySet *documents; +- (const firebase::firestore::model::DocumentKeySet &)documents; @end #pragma mark - FSTUpdateMapping @@ -74,12 +83,13 @@ NS_ASSUME_NONNULL_BEGIN + (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added removedDocuments:(NSArray<FSTDocument *> *)removed; -- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys; +- (firebase::firestore::model::DocumentKeySet)applyTo: + (const firebase::firestore::model::DocumentKeySet &)keys; /** The documents added to the target. */ -@property(nonatomic, strong, readonly) FSTDocumentKeySet *addedDocuments; +- (const firebase::firestore::model::DocumentKeySet &)addedDocuments; /** The documents removed from the target. */ -@property(nonatomic, strong, readonly) FSTDocumentKeySet *removedDocuments; +- (const firebase::firestore::model::DocumentKeySet &)removedDocuments; @end #pragma mark - FSTTargetChange @@ -107,6 +117,12 @@ typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { @interface FSTTargetChange : NSObject /** + * Creates a new target change with the given SnapshotVersion. + */ +- (instancetype)initWithSnapshotVersion: + (firebase::firestore::model::SnapshotVersion)snapshotVersion; + +/** * 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. @@ -115,6 +131,12 @@ typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate; /** + * The snapshot version representing the last state at which this target received a consistent + * snapshot from the backend. + */ +- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion; + +/** * 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. @@ -125,12 +147,6 @@ typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { @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. @@ -147,14 +163,15 @@ typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { */ @interface FSTRemoteEvent : NSObject -+ (instancetype) -eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges - documentUpdates: - (std::map<firebase::firestore::model::DocumentKey, FSTMaybeDocument *>)documentUpdates; +- (instancetype) +initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion + targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges + documentUpdates: + (std::map<firebase::firestore::model::DocumentKey, FSTMaybeDocument *>)documentUpdates + limboDocuments:(firebase::firestore::model::DocumentKeySet)limboDocuments; /** The snapshot version this event brings us up to. */ -@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; +- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion; /** A map from target to changes to the target. See TargetChange. */ @property(nonatomic, strong, readonly) @@ -166,6 +183,8 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion */ - (const std::map<firebase::firestore::model::DocumentKey, FSTMaybeDocument *> &)documentUpdates; +- (const firebase::firestore::model::DocumentKeySet &)limboDocumentChanges; + /** Adds a document update to this remote event */ - (void)addDocumentUpdate:(FSTMaybeDocument *)document; @@ -175,14 +194,6 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - (void)synthesizeDeleteForLimboTargetChange:(FSTTargetChange *)targetChange key:(const firebase::firestore::model::DocumentKey &)key; -/** - * Strips out mapping changes that aren't actually changes. That is, if the document already - * existed in the target, and is being added in the target, and this is not a reset, we can - * skip doing the work to associate the document with the target because it has already been done. - */ -- (void)filterUpdatesFromTargetChange:(FSTTargetChange *)targetChange - existingDocuments:(FSTDocumentKeySet *)existingDocuments; - @end #pragma mark - FSTWatchChangeAggregator @@ -194,7 +205,7 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion @interface FSTWatchChangeAggregator : NSObject - (instancetype) -initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion +initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses NS_DESIGNATED_INITIALIZER; diff --git a/Firestore/Source/Remote/FSTRemoteEvent.mm b/Firestore/Source/Remote/FSTRemoteEvent.mm index 30aa0d3..438072e 100644 --- a/Firestore/Source/Remote/FSTRemoteEvent.mm +++ b/Firestore/Source/Remote/FSTRemoteEvent.mm @@ -19,16 +19,19 @@ #include <map> #include <utility> -#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Remote/FSTWatchChange.h" #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTClasses.h" #import "Firestore/Source/Util/FSTLogger.h" - #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::util::Hash; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -54,32 +57,38 @@ NS_ASSUME_NONNULL_BEGIN @throw FSTAbstractMethodException(); // NOLINT } +- (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys { + @throw FSTAbstractMethodException(); // NOLINT +} + @end #pragma mark - FSTResetMapping -@interface FSTResetMapping () -@property(nonatomic, strong) FSTDocumentKeySet *documents; -@end - -@implementation FSTResetMapping +@implementation FSTResetMapping { + DocumentKeySet _documents; +} + (instancetype)mappingWithDocuments:(NSArray<FSTDocument *> *)documents { - FSTResetMapping *mapping = [[FSTResetMapping alloc] init]; + DocumentKeySet keys; for (FSTDocument *doc in documents) { - mapping.documents = [mapping.documents setByAddingObject:doc.key]; + keys = keys.insert(doc.key); } - return mapping; + return [[FSTResetMapping alloc] initWithDocuments:std::move(keys)]; } -- (instancetype)init { +- (instancetype)initWithDocuments:(DocumentKeySet)documents { self = [super init]; if (self) { - _documents = [FSTDocumentKeySet keySet]; + _documents = std::move(documents); } return self; } +- (const DocumentKeySet &)documents { + return _documents; +} + - (BOOL)isEqual:(id)other { if (other == self) { return YES; @@ -89,53 +98,66 @@ NS_ASSUME_NONNULL_BEGIN } FSTResetMapping *otherMapping = (FSTResetMapping *)other; - return [self.documents isEqual:otherMapping.documents]; + return _documents == otherMapping.documents; } - (NSUInteger)hash { - return self.documents.hash; + return Hash(_documents); } - (void)addDocumentKey:(const DocumentKey &)documentKey { - self.documents = [self.documents setByAddingObject:documentKey]; + _documents = _documents.insert(documentKey); } - (void)removeDocumentKey:(const DocumentKey &)documentKey { - self.documents = [self.documents setByRemovingObject:documentKey]; + _documents = _documents.erase(documentKey); +} + +- (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys { + // No-op. Resets are not filtered. } @end #pragma mark - FSTUpdateMapping -@interface FSTUpdateMapping () -@property(nonatomic, strong) FSTDocumentKeySet *addedDocuments; -@property(nonatomic, strong) FSTDocumentKeySet *removedDocuments; -@end - -@implementation FSTUpdateMapping +@implementation FSTUpdateMapping { + DocumentKeySet _addedDocuments; + DocumentKeySet _removedDocuments; +} + (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added removedDocuments:(NSArray<FSTDocument *> *)removed { - FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + DocumentKeySet addedDocuments; + DocumentKeySet removedDocuments; for (FSTDocument *doc in added) { - mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + addedDocuments = addedDocuments.insert(doc.key); } for (FSTDocument *doc in removed) { - mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + removedDocuments = removedDocuments.insert(doc.key); } - return mapping; + return [[FSTUpdateMapping alloc] initWithAddedDocuments:std::move(addedDocuments) + removedDocuments:std::move(removedDocuments)]; } -- (instancetype)init { +- (instancetype)initWithAddedDocuments:(DocumentKeySet)addedDocuments + removedDocuments:(DocumentKeySet)removedDocuments { self = [super init]; if (self) { - _addedDocuments = [FSTDocumentKeySet keySet]; - _removedDocuments = [FSTDocumentKeySet keySet]; + _addedDocuments = std::move(addedDocuments); + _removedDocuments = std::move(removedDocuments); } return self; } +- (const DocumentKeySet &)addedDocuments { + return _addedDocuments; +} + +- (const DocumentKeySet &)removedDocuments { + return _removedDocuments; +} + - (BOOL)isEqual:(id)other { if (other == self) { return YES; @@ -145,33 +167,43 @@ NS_ASSUME_NONNULL_BEGIN } FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other; - return [self.addedDocuments isEqual:otherMapping.addedDocuments] && - [self.removedDocuments isEqual:otherMapping.removedDocuments]; + return _addedDocuments == otherMapping.addedDocuments && + _removedDocuments == otherMapping.removedDocuments; } - (NSUInteger)hash { - return self.addedDocuments.hash * 31 + self.removedDocuments.hash; + return Hash(_addedDocuments, _removedDocuments); } -- (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]; - }]; +- (DocumentKeySet)applyTo:(const DocumentKeySet &)keys { + DocumentKeySet result = keys; + for (const DocumentKey &key : _addedDocuments) { + result = result.insert(key); + } + for (const DocumentKey &key : _removedDocuments) { + result = result.erase(key); + } return result; } - (void)addDocumentKey:(const DocumentKey &)documentKey { - self.addedDocuments = [self.addedDocuments setByAddingObject:documentKey]; - self.removedDocuments = [self.removedDocuments setByRemovingObject:documentKey]; + _addedDocuments = _addedDocuments.insert(documentKey); + _removedDocuments = _removedDocuments.erase(documentKey); } - (void)removeDocumentKey:(const DocumentKey &)documentKey { - self.addedDocuments = [self.addedDocuments setByRemovingObject:documentKey]; - self.removedDocuments = [self.removedDocuments setByAddingObject:documentKey]; + _addedDocuments = _addedDocuments.erase(documentKey); + _removedDocuments = _removedDocuments.insert(documentKey); +} + +- (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys { + DocumentKeySet result = _addedDocuments; + for (const DocumentKey &key : _addedDocuments) { + if (existingKeys.contains(key)) { + result = result.erase(key); + } + } + _addedDocuments = result; } @end @@ -181,11 +213,12 @@ NS_ASSUME_NONNULL_BEGIN @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 +@implementation FSTTargetChange { + SnapshotVersion _snapshotVersion; +} - (instancetype)init { if (self = [super init]) { @@ -195,16 +228,32 @@ NS_ASSUME_NONNULL_BEGIN return self; } +- (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion { + if (self = [self init]) { + _snapshotVersion = std::move(snapshotVersion); + } + return self; +} + +- (const SnapshotVersion &)snapshotVersion { + return _snapshotVersion; +} + + (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { - FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + DocumentKeySet addedDocuments; + DocumentKeySet removedDocuments; for (FSTMaybeDocument *doc in docs) { if ([doc isKindOfClass:[FSTDeletedDocument class]]) { - mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + removedDocuments = removedDocuments.insert(doc.key); } else { - mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + addedDocuments = addedDocuments.insert(doc.key); } } + FSTUpdateMapping *mapping = + [[FSTUpdateMapping alloc] initWithAddedDocuments:std::move(addedDocuments) + removedDocuments:std::move(removedDocuments)]; + FSTTargetChange *change = [[FSTTargetChange alloc] init]; change.mapping = mapping; change.currentStatusUpdate = currentStatusUpdate; @@ -212,11 +261,11 @@ NS_ASSUME_NONNULL_BEGIN } + (instancetype)changeWithMapping:(FSTTargetMapping *)mapping - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion:(SnapshotVersion)snapshotVersion currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { FSTTargetChange *change = [[FSTTargetChange alloc] init]; change.mapping = mapping; - change.snapshotVersion = snapshotVersion; + change->_snapshotVersion = std::move(snapshotVersion); change.currentStatusUpdate = currentStatusUpdate; return change; } @@ -243,57 +292,42 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - FSTRemoteEvent -@interface FSTRemoteEvent () { - NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *_targetChanges; -} - -- (instancetype) -initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - targetChanges:(NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges - documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates; - -@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; - -@end - @implementation FSTRemoteEvent { + SnapshotVersion _snapshotVersion; + NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *_targetChanges; std::map<DocumentKey, FSTMaybeDocument *> _documentUpdates; -} -+ (instancetype) -eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges - documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates { - return [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion - targetChanges:targetChanges - documentUpdates:std::move(documentUpdates)]; + DocumentKeySet _limboDocumentChanges; } -- (instancetype)initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion +- (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion targetChanges: (NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges - documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates { + documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates + limboDocuments:(DocumentKeySet)limboDocuments { self = [super init]; if (self) { - _snapshotVersion = snapshotVersion; + _snapshotVersion = std::move(snapshotVersion); _targetChanges = targetChanges; _documentUpdates = std::move(documentUpdates); + _limboDocumentChanges = std::move(limboDocuments); } return self; } -- (void)filterUpdatesFromTargetChange:(FSTTargetChange *)targetChange - existingDocuments:(FSTDocumentKeySet *)existingDocuments { - if ([targetChange.mapping isKindOfClass:[FSTUpdateMapping class]]) { - FSTUpdateMapping *update = (FSTUpdateMapping *)targetChange.mapping; - FSTDocumentKeySet *added = update.addedDocuments; - __block FSTDocumentKeySet *result = added; - [added enumerateObjectsUsingBlock:^(FSTDocumentKey *docKey, BOOL *stop) { - if ([existingDocuments containsObject:docKey]) { - result = [result setByRemovingObject:docKey]; - } - }]; - update.addedDocuments = result; - } +- (NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges { + return _targetChanges; +} + +- (const DocumentKeySet &)limboDocumentChanges { + return _limboDocumentChanges; +} + +- (const std::map<DocumentKey, FSTMaybeDocument *> &)documentUpdates { + return _documentUpdates; +} + +- (const SnapshotVersion &)snapshotVersion { + return _snapshotVersion; } - (void)synthesizeDeleteForLimboTargetChange:(FSTTargetChange *)targetChange @@ -314,22 +348,15 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion // 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. + // we specially handle 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. _documentUpdates[key] = [FSTDeletedDocument documentWithKey:key version:_snapshotVersion]; + _limboDocumentChanges = _limboDocumentChanges.insert(key); } } -- (NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges { - return static_cast<NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *>(_targetChanges); -} - -- (const std::map<DocumentKey, FSTMaybeDocument *> &)documentUpdates { - return _documentUpdates; -} - /** Adds a document update to this remote event */ - (void)addDocumentUpdate:(FSTMaybeDocument *)document { _documentUpdates[document.key] = document; @@ -350,7 +377,7 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion // TODO(dimond): keep track of reset targets not to raise. FSTTargetChange *targetChange = [FSTTargetChange changeWithMapping:[[FSTResetMapping alloc] init] - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() currentStatusUpdate:FSTCurrentStatusUpdateMarkNotCurrent]; _targetChanges[targetID] = targetChange; } @@ -361,9 +388,6 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion @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; @@ -381,35 +405,38 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion NSMutableDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *_existenceFilters; /** Keeps track of document to update */ std::map<DocumentKey, FSTMaybeDocument *> _documentUpdates; + + DocumentKeySet _limboDocuments; + /** The snapshot version for every target change this creates. */ + SnapshotVersion _snapshotVersion; } - (instancetype) -initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion +initWithSnapshotVersion:(SnapshotVersion)snapshotVersion listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses { self = [super init]; if (self) { - _snapshotVersion = snapshotVersion; + _snapshotVersion = std::move(snapshotVersion); _frozen = NO; _targetChanges = [NSMutableDictionary dictionary]; _listenTargets = listenTargets; _pendingTargetResponses = [NSMutableDictionary dictionaryWithDictionary:pendingTargetResponses]; - + _limboDocuments = DocumentKeySet{}; _existenceFilters = [NSMutableDictionary dictionary]; } return self; } - (NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *)existenceFilters { - return static_cast<NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *>(_existenceFilters); + return _existenceFilters; } - (FSTTargetChange *)targetChangeForTargetID:(FSTBoxedTargetID *)targetID { FSTTargetChange *change = self.targetChanges[targetID]; if (!change) { - change = [[FSTTargetChange alloc] init]; - change.snapshotVersion = self.snapshotVersion; + change = [[FSTTargetChange alloc] initWithSnapshotVersion:_snapshotVersion]; self.targetChanges[targetID] = change; } return change; @@ -435,20 +462,66 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion } } +/** + * Updates limbo document tracking for a given target-document mapping change. If the target is a + * limbo target, and the change for the document has only seen limbo targets so far, and we are not + * already tracking a change for this document, then consider this document a limbo document update. + * Otherwise, ensure that we don't consider this document a limbo document. Returns true if the + * change still has only seen limbo resolution changes. + */ +- (BOOL)updateLimboDocumentsForKey:(const DocumentKey &)documentKey + queryData:(FSTQueryData *)queryData + isOnlyLimbo:(BOOL)isOnlyLimbo { + if (!isOnlyLimbo) { + // It wasn't a limbo doc before, so it definitely isn't now. + return NO; + } + if (_documentUpdates.find(documentKey) == _documentUpdates.end()) { + // We haven't seen a document update for this key yet. + if (queryData.purpose == FSTQueryPurposeLimboResolution) { + // We haven't seen this document before, and this target is a limbo target. + _limboDocuments = _limboDocuments.insert(documentKey); + return YES; + } else { + // We haven't seen the document before, but this is a non-limbo target. + // Since we haven't seen it, we know it's not in our set of limbo docs. Return NO to ensure + // that this key is marked as non-limbo. + return NO; + } + } else if (queryData.purpose == FSTQueryPurposeLimboResolution) { + // We have only seen limbo targets so far for this document, and this is another limbo target. + return YES; + } else { + // We haven't marked this as non-limbo yet, but this target is not a limbo target. + // Mark the key as non-limbo and make sure it isn't in our set. + _limboDocuments = _limboDocuments.erase(documentKey); + return NO; + } +} + - (void)addDocumentChange:(FSTDocumentWatchChange *)docChange { BOOL relevant = NO; + BOOL isOnlyLimbo = YES; for (FSTBoxedTargetID *targetID in docChange.updatedTargetIDs) { - if ([self isActiveTarget:targetID]) { + FSTQueryData *queryData = [self queryDataForActiveTarget:targetID]; + if (queryData) { FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + isOnlyLimbo = [self updateLimboDocumentsForKey:docChange.documentKey + queryData:queryData + isOnlyLimbo:isOnlyLimbo]; [change.mapping addDocumentKey:docChange.documentKey]; relevant = YES; } } for (FSTBoxedTargetID *targetID in docChange.removedTargetIDs) { - if ([self isActiveTarget:targetID]) { + FSTQueryData *queryData = [self queryDataForActiveTarget:targetID]; + if (queryData) { FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + isOnlyLimbo = [self updateLimboDocumentsForKey:docChange.documentKey + queryData:queryData + isOnlyLimbo:isOnlyLimbo]; [change.mapping removeDocumentKey:docChange.documentKey]; relevant = YES; } @@ -473,7 +546,7 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion break; case FSTWatchTargetChangeStateAdded: [self recordResponseForTargetID:targetID]; - if (![self.pendingTargetResponses objectForKey:targetID]) { + if (!self.pendingTargetResponses[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. @@ -514,12 +587,12 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion * responses that we have. */ - (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID { - NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; + NSNumber *count = self.pendingTargetResponses[targetID]; int newCount = count ? [count intValue] - 1 : -1; if (newCount == 0) { [self.pendingTargetResponses removeObjectForKey:targetID]; } else { - [self.pendingTargetResponses setObject:[NSNumber numberWithInt:newCount] forKey:targetID]; + self.pendingTargetResponses[targetID] = @(newCount); } } @@ -532,8 +605,12 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion * yet acknowledged the intended change in state. */ - (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID { - return [self.listenTargets objectForKey:targetID] && - ![self.pendingTargetResponses objectForKey:targetID]; + return [self queryDataForActiveTarget:targetID] != nil; +} + +- (FSTQueryData *_Nullable)queryDataForActiveTarget:(FSTBoxedTargetID *)targetID { + FSTQueryData *queryData = self.listenTargets[targetID]; + return (queryData && !self.pendingTargetResponses[targetID]) ? queryData : nil; } - (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange { @@ -559,9 +636,10 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion // Mark this aggregator as frozen so no further modifications are made. self.frozen = YES; - return [FSTRemoteEvent eventWithSnapshotVersion:self.snapshotVersion - targetChanges:targetChanges - documentUpdates:_documentUpdates]; + return [[FSTRemoteEvent alloc] initWithSnapshotVersion:_snapshotVersion + targetChanges:targetChanges + documentUpdates:_documentUpdates + limboDocuments:_limboDocuments]; } @end diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h index 09e1d32..9b01ce4 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.h +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -17,7 +17,6 @@ #import <Foundation/Foundation.h> #import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Model/FSTDocumentVersionDictionary.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index 39d285a..0ea4887 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -19,7 +19,6 @@ #include <cinttypes> #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Core/FSTTransaction.h" #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Local/FSTQueryData.h" @@ -37,11 +36,14 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; using firebase::firestore::auth::User; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -299,7 +301,7 @@ static const int kMaxPendingWrites = 10; } - (void)watchStreamDidChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + snapshotVersion:(const SnapshotVersion &)snapshotVersion { // Mark the connection as Online because we got a message from the server. [self.onlineStateTracker updateState:FSTOnlineStateOnline]; @@ -315,10 +317,8 @@ static const int kMaxPendingWrites = 10; // 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) { + if (snapshotVersion == SnapshotVersion::None() || + snapshotVersion < [self.localStore lastRemoteSnapshotVersion]) { return; } @@ -354,7 +354,7 @@ static const int kMaxPendingWrites = 10; * on to the SyncEngine. */ - (void)processBatchedWatchChanges:(NSArray<FSTWatchChange *> *)changes - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + snapshotVersion:(const SnapshotVersion &)snapshotVersion { FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:snapshotVersion listenTargets:self.listenTargets @@ -394,7 +394,7 @@ static const int kMaxPendingWrites = 10; } else { // Not a document query. - FSTDocumentKeySet *trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID]; + DocumentKeySet trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID]; FSTTargetMapping *mapping = remoteEvent.targetChanges[target].mapping; if (mapping) { if ([mapping isKindOfClass:[FSTUpdateMapping class]]) { @@ -407,7 +407,7 @@ static const int kMaxPendingWrites = 10; } } - if (trackedRemote.count != (NSUInteger)filter.count) { + if (trackedRemote.size() != static_cast<size_t>(filter.count)) { FSTLog(@"Existence filter mismatch, resetting mapping"); // Make sure the mismatch is exposed in the remote event @@ -449,7 +449,8 @@ static const int kMaxPendingWrites = 10; if (queryData) { self->_listenTargets[target] = [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion - resumeToken:resumeToken]; + resumeToken:resumeToken + sequenceNumber:queryData.sequenceNumber]; } } }]; @@ -566,7 +567,7 @@ static const int kMaxPendingWrites = 10; } /** Handles a successful StreamingWriteResponse from the server that contains a mutation result. */ -- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)writeStreamDidReceiveResponseWithVersion:(const SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results { // This is a response to a write containing mutations and should be correlated to the first // pending write. diff --git a/Firestore/Source/Remote/FSTSerializerBeta.h b/Firestore/Source/Remote/FSTSerializerBeta.h index d96dbeb..cdf5d1f 100644 --- a/Firestore/Source/Remote/FSTSerializerBeta.h +++ b/Firestore/Source/Remote/FSTSerializerBeta.h @@ -16,8 +16,10 @@ #import <Foundation/Foundation.h> +#include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTFieldValue; @class FSTMaybeDocument; @@ -27,8 +29,6 @@ @class FSTObjectValue; @class FSTQuery; @class FSTQueryData; -@class FSTSnapshotVersion; -@class FIRTimestamp; @class FSTWatchChange; @class GCFSBatchGetDocumentsResponse; @@ -61,15 +61,19 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithDatabaseID:(const firebase::firestore::model::DatabaseId *)databaseID NS_DESIGNATED_INITIALIZER; -- (GPBTimestamp *)encodedTimestamp:(FIRTimestamp *)timestamp; -- (FIRTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp; +- (GPBTimestamp *)encodedTimestamp:(const firebase::Timestamp &)timestamp; +- (firebase::Timestamp)decodedTimestamp:(GPBTimestamp *)timestamp; -- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version; -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version; +- (GPBTimestamp *)encodedVersion:(const firebase::firestore::model::SnapshotVersion &)version; +- (firebase::firestore::model::SnapshotVersion)decodedVersion:(GPBTimestamp *)version; /** Returns the database ID, such as `projects/{project id}/databases/{database_id}`. */ - (NSString *)encodedDatabaseID; +/** + * Encodes the given document key as a fully qualified name. This includes the + * databaseId associated with this FSTSerializerBeta and the key path. + */ - (NSString *)encodedDocumentKey:(const firebase::firestore::model::DocumentKey &)key; - (firebase::firestore::model::DocumentKey)decodedDocumentKey:(NSString *)key; @@ -93,7 +97,8 @@ NS_ASSUME_NONNULL_BEGIN - (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target; - (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange; -- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange; +- (firebase::firestore::model::SnapshotVersion)versionFromListenResponse: + (GCFSListenResponse *)watchChange; - (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue key:(const firebase::firestore::model::DocumentKey &)key; diff --git a/Firestore/Source/Remote/FSTSerializerBeta.mm b/Firestore/Source/Remote/FSTSerializerBeta.mm index 5cbfecc..782e54c 100644 --- a/Firestore/Source/Remote/FSTSerializerBeta.mm +++ b/Firestore/Source/Remote/FSTSerializerBeta.mm @@ -33,9 +33,7 @@ #import "FIRFirestoreErrors.h" #import "FIRGeoPoint.h" -#import "FIRTimestamp.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" @@ -55,8 +53,10 @@ #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "absl/memory/memory.h" +#include "absl/types/optional.h" namespace util = firebase::firestore::util; +using firebase::Timestamp; using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; @@ -86,25 +86,25 @@ NS_ASSUME_NONNULL_BEGIN return self; } -#pragma mark - FSTSnapshotVersion <=> GPBTimestamp +#pragma mark - SnapshotVersion <=> GPBTimestamp -- (GPBTimestamp *)encodedTimestamp:(FIRTimestamp *)timestamp { +- (GPBTimestamp *)encodedTimestamp:(const Timestamp &)timestamp { GPBTimestamp *result = [GPBTimestamp message]; - result.seconds = timestamp.seconds; - result.nanos = timestamp.nanoseconds; + result.seconds = timestamp.seconds(); + result.nanos = timestamp.nanoseconds(); return result; } -- (FIRTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp { - return [[FIRTimestamp alloc] initWithSeconds:timestamp.seconds nanoseconds:timestamp.nanos]; +- (Timestamp)decodedTimestamp:(GPBTimestamp *)timestamp { + return Timestamp{timestamp.seconds, timestamp.nanos}; } -- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { - return [self encodedTimestamp:version.timestamp]; +- (GPBTimestamp *)encodedVersion:(const SnapshotVersion &)version { + return [self encodedTimestamp:version.timestamp()]; } -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { - return [FSTSnapshotVersion versionWithTimestamp:[self decodedTimestamp:version]]; +- (SnapshotVersion)decodedVersion:(GPBTimestamp *)version { + return SnapshotVersion{[self decodedTimestamp:version]}; } #pragma mark - FIRGeoPoint <=> GTPLatLng @@ -206,8 +206,8 @@ NS_ASSUME_NONNULL_BEGIN return [self encodedString:[fieldValue value]]; } else if (fieldClass == [FSTTimestampValue class]) { - return [self encodedTimestampValue:[fieldValue value]]; - + FIRTimestamp *value = static_cast<FIRTimestamp *>([fieldValue value]); + return [self encodedTimestampValue:Timestamp{value.seconds, value.nanoseconds}]; } else if (fieldClass == [FSTGeoPointValue class]) { return [self encodedGeoPointValue:[fieldValue value]]; @@ -250,8 +250,12 @@ NS_ASSUME_NONNULL_BEGIN 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_TimestampValue: { + Timestamp value = [self decodedTimestamp:valueProto.timestampValue]; + return [FSTTimestampValue + timestampValue:[FIRTimestamp timestampWithSeconds:value.seconds() + nanoseconds:value.nanoseconds()]]; + } case GCFSValue_ValueType_OneOfCase_GeoPointValue: return [FSTGeoPointValue geoPointValue:[self decodedGeoPoint:valueProto.geoPointValue]]; @@ -303,7 +307,7 @@ NS_ASSUME_NONNULL_BEGIN return result; } -- (GCFSValue *)encodedTimestampValue:(FIRTimestamp *)value { +- (GCFSValue *)encodedTimestampValue:(const Timestamp &)value { GCFSValue *result = [GCFSValue message]; result.timestampValue = [self encodedTimestamp:value]; return result; @@ -429,8 +433,8 @@ NS_ASSUME_NONNULL_BEGIN FSTAssert(!!response.found, @"Tried to deserialize a found document from a deleted document."); const DocumentKey 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]], + SnapshotVersion version = [self decodedVersion:response.found.updateTime]; + FSTAssert(version != SnapshotVersion::None(), @"Got a document response with no snapshot version"); return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; @@ -439,8 +443,8 @@ NS_ASSUME_NONNULL_BEGIN - (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response { FSTAssert(!!response.missing, @"Tried to deserialize a deleted document from a found document."); const DocumentKey key = [self decodedDocumentKey:response.missing]; - FSTSnapshotVersion *version = [self decodedVersion:response.readTime]; - FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + SnapshotVersion version = [self decodedVersion:response.readTime]; + FSTAssert(version != SnapshotVersion::None(), @"Got a no document response with no snapshot version"); return [FSTDeletedDocument documentWithKey:key version:version]; } @@ -668,8 +672,10 @@ NS_ASSUME_NONNULL_BEGIN - (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation { // NOTE: Deletes don't have an updateTime. - FSTSnapshotVersion *_Nullable version = - mutation.updateTime ? [self decodedVersion:mutation.updateTime] : nil; + absl::optional<SnapshotVersion> version; + if (mutation.updateTime) { + version = [self decodedVersion:mutation.updateTime]; + } NSMutableArray *_Nullable transformResults = nil; if (mutation.transformResultsArray.count > 0) { transformResults = [NSMutableArray array]; @@ -677,7 +683,8 @@ NS_ASSUME_NONNULL_BEGIN [transformResults addObject:[self decodedFieldValue:result]]; } } - return [[FSTMutationResult alloc] initWithVersion:version transformResults:transformResults]; + return [[FSTMutationResult alloc] + initWithVersion:(version ? version.value() : nil)transformResults:transformResults]; } #pragma mark - FSTQueryData => GCFSTarget proto @@ -1071,15 +1078,15 @@ NS_ASSUME_NONNULL_BEGIN } } -- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange { +- (SnapshotVersion)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]; + return SnapshotVersion::None(); } if (watchChange.targetChange.targetIdsArray.count != 0) { - return [FSTSnapshotVersion noVersion]; + return SnapshotVersion::None(); } return [self decodedVersion:watchChange.targetChange.readTime]; } @@ -1135,9 +1142,8 @@ NS_ASSUME_NONNULL_BEGIN - (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change { FSTObjectValue *value = [self decodedFields:change.document.fields]; const DocumentKey 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"); + SnapshotVersion version = [self decodedVersion:change.document.updateTime]; + FSTAssert(version != SnapshotVersion::None(), @"Got a document change with no snapshot version"); FSTMaybeDocument *document = [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; @@ -1152,8 +1158,8 @@ NS_ASSUME_NONNULL_BEGIN - (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change { const DocumentKey key = [self decodedDocumentKey:change.document]; - // Note that version might be unset in which case we use [FSTSnapshotVersion noVersion] - FSTSnapshotVersion *version = [self decodedVersion:change.readTime]; + // Note that version might be unset in which case we use SnapshotVersion::None() + SnapshotVersion version = [self decodedVersion:change.readTime]; FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version]; NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; diff --git a/Firestore/Source/Remote/FSTStream.h b/Firestore/Source/Remote/FSTStream.h index fba79d2..3bd8549 100644 --- a/Firestore/Source/Remote/FSTStream.h +++ b/Firestore/Source/Remote/FSTStream.h @@ -21,13 +21,13 @@ #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTDispatchQueue; @class FSTMutation; @class FSTMutationResult; @class FSTQueryData; @class FSTSerializerBeta; -@class FSTSnapshotVersion; @class FSTWatchChange; @class FSTWatchStream; @class FSTWriteStream; @@ -179,7 +179,7 @@ NS_ASSUME_NONNULL_BEGIN * WatchChange responses sent back by the server. */ - (void)watchStreamDidChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion; + snapshotVersion:(const firebase::firestore::model::SnapshotVersion &)snapshotVersion; /** * Called by the FSTWatchStream when the underlying streaming RPC is interrupted for whatever @@ -250,7 +250,8 @@ NS_ASSUME_NONNULL_BEGIN * Called by the FSTWriteStream upon receiving a StreamingWriteResponse from the server that * contains mutation results. */ -- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)writeStreamDidReceiveResponseWithVersion: + (const firebase::firestore::model::SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results; /** diff --git a/Firestore/Source/Remote/FSTStream.mm b/Firestore/Source/Remote/FSTStream.mm index a96feae..f4ec675 100644 --- a/Firestore/Source/Remote/FSTStream.mm +++ b/Firestore/Source/Remote/FSTStream.mm @@ -36,6 +36,7 @@ #include "Firestore/core/src/firebase/firestore/auth/token.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" @@ -44,6 +45,7 @@ using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::Token; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::SnapshotVersion; /** * Initial backoff time in seconds after an error. @@ -691,7 +693,7 @@ static const NSTimeInterval kIdleTimeout = 60.0; [self.backoff reset]; FSTWatchChange *change = [_serializer decodedWatchChange:proto]; - FSTSnapshotVersion *snap = [_serializer versionFromListenResponse:proto]; + SnapshotVersion snap = [_serializer versionFromListenResponse:proto]; [self.delegate watchStreamDidChange:change snapshotVersion:snap]; } @@ -807,7 +809,7 @@ static const NSTimeInterval kIdleTimeout = 60.0; // might be causing an error we want to back off from. [self.backoff reset]; - FSTSnapshotVersion *commitVersion = [_serializer decodedVersion:response.commitTime]; + SnapshotVersion commitVersion = [_serializer decodedVersion:response.commitTime]; NSMutableArray<GCFSWriteResult *> *protos = response.writeResultsArray; NSMutableArray<FSTMutationResult *> *results = [NSMutableArray arrayWithCapacity:protos.count]; for (GCFSWriteResult *proto in protos) { diff --git a/Firestore/Source/Remote/FSTWatchChange.h b/Firestore/Source/Remote/FSTWatchChange.h index 8f730de..ed80e1a 100644 --- a/Firestore/Source/Remote/FSTWatchChange.h +++ b/Firestore/Source/Remote/FSTWatchChange.h @@ -22,7 +22,6 @@ @class FSTExistenceFilter; @class FSTMaybeDocument; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN diff --git a/Firestore/core/src/firebase/firestore/immutable/CMakeLists.txt b/Firestore/core/src/firebase/firestore/immutable/CMakeLists.txt index 90ce204..af97b62 100644 --- a/Firestore/core/src/firebase/firestore/immutable/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/immutable/CMakeLists.txt @@ -24,6 +24,7 @@ cc_library( sorted_map_base.h sorted_map_base.cc sorted_map_iterator.h + sorted_set.h tree_sorted_map.h DEPENDS firebase_firestore_util diff --git a/Firestore/core/src/firebase/firestore/immutable/sorted_set.h b/Firestore/core/src/firebase/firestore/immutable/sorted_set.h new file mode 100644 index 0000000..d78fd61 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/immutable/sorted_set.h @@ -0,0 +1,157 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_SET_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_SET_H_ + +#include <algorithm> +#include <utility> + +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h" +#include "Firestore/core/src/firebase/firestore/util/comparison.h" +#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" + +namespace firebase { +namespace firestore { +namespace immutable { + +namespace impl { + +// An empty value to associate with keys in the underlying map. +struct Empty { + friend bool operator==(Empty /* left */, Empty /* right */) { + return true; + } +}; + +} // namespace impl + +template <typename K, + typename V = impl::Empty, + typename C = util::Comparator<K>, + typename M = SortedMap<K, V, C>> +class SortedSet { + public: + using size_type = typename M::size_type; + using value_type = K; + + using const_iterator = typename M::const_key_iterator; + + explicit SortedSet(const C& comparator = C()) : map_{comparator} { + } + + explicit SortedSet(const M& map) : map_{map} { + } + + explicit SortedSet(M&& map) : map_{std::move(map)} { + } + + SortedSet(std::initializer_list<value_type> entries, const C& comparator = {}) + : map_{comparator} { + for (auto&& value : entries) { + map_ = map_.insert(value, {}); + } + } + + bool empty() const { + return map_.empty(); + } + + size_type size() const { + return map_.size(); + } + + SortedSet insert(const K& key) const { + return SortedSet{map_.insert(key, {})}; + } + + SortedSet erase(const K& key) const { + return SortedSet{map_.erase(key)}; + } + + bool contains(const K& key) const { + return map_.contains(key); + } + + const_iterator find(const K& key) const { + return const_iterator{map_.find(key)}; + } + + size_type find_index(const K& key) const { + return map_.find_index(key); + } + + const_iterator min() const { + return const_iterator{map_.min()}; + } + + const K& max() const { + return const_iterator{map_.max()}; + } + + const_iterator begin() const { + return const_iterator{map_.begin()}; + } + + const_iterator end() const { + return const_iterator{map_.end()}; + } + + /** + * Returns a view of this SortedSet containing just the keys that have been + * inserted that are greater than or equal to the given key. + */ + const util::range<const_iterator> values_from(const K& key) const { + return map_.keys_from(key); + } + + /** + * Returns a view of this SortedSet containing just the keys that have been + * inserted that are greater than or equal to the given start_key and less + * than the given end_key. + */ + const util::range<const_iterator> values_in(const K& start_key, + const K& end_key) const { + return map_.keys_in(start_key, end_key); + } + + friend bool operator==(const SortedSet& lhs, const SortedSet& rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + return std::equal(lhs.begin(), lhs.end(), rhs.begin()); + } + + friend bool operator!=(const SortedSet& lhs, const SortedSet& rhs) { + return !(lhs == rhs); + } + + private: + M map_; +}; + +template <typename K, typename V, typename C> +SortedSet<K, V, C> MakeSortedSet(const SortedMap<K, V, C>& map) { + return SortedSet<K, V, C>{map}; +} + +} // namespace immutable +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_SET_H_ diff --git a/Firestore/core/src/firebase/firestore/model/base_path.h b/Firestore/core/src/firebase/firestore/model/base_path.h index bc1f89d..58df6f0 100644 --- a/Firestore/core/src/firebase/firestore/model/base_path.h +++ b/Firestore/core/src/firebase/firestore/model/base_path.h @@ -25,6 +25,7 @@ #include <vector> #include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" namespace firebase { namespace firestore { @@ -159,19 +160,6 @@ class BasePath { return segments_ >= rhs.segments_; } -#if defined(__OBJC__) - // For Objective-C++ hash; to be removed after migration. - // Do NOT use in C++ code. - NSUInteger Hash() const { - std::hash<std::string> hash_fn; - NSUInteger hash_result = 0; - for (const std::string& segment : segments_) { - hash_result = hash_result * 31u + hash_fn(segment); - } - return hash_result; - } -#endif // defined(__OBJC__) - protected: BasePath() = default; template <typename IterT> diff --git a/Firestore/core/src/firebase/firestore/model/database_id.h b/Firestore/core/src/firebase/firestore/model/database_id.h index 0c0e0ec..c432b8f 100644 --- a/Firestore/core/src/firebase/firestore/model/database_id.h +++ b/Firestore/core/src/firebase/firestore/model/database_id.h @@ -20,6 +20,7 @@ #include <cstdint> #include <string> +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "absl/strings/string_view.h" namespace firebase { @@ -62,9 +63,8 @@ class DatabaseId { #if defined(__OBJC__) // For objective-c++ hash; to be removed after migration. // Do NOT use in C++ code. - NSUInteger Hash() const { - std::hash<std::string> hash_fn; - return hash_fn(project_id_) * 31u + hash_fn(database_id_); + size_t Hash() const { + return util::Hash(project_id_, database_id_); } #endif // defined(__OBJC__) diff --git a/Firestore/core/src/firebase/firestore/model/document_key.h b/Firestore/core/src/firebase/firestore/model/document_key.h index 4bdc04b..3f5f342 100644 --- a/Firestore/core/src/firebase/firestore/model/document_key.h +++ b/Firestore/core/src/firebase/firestore/model/document_key.h @@ -17,6 +17,7 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_H_ +#include <functional> #include <initializer_list> #include <memory> #include <string> @@ -26,6 +27,8 @@ #endif // defined(__OBJC__) #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/comparison.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "absl/strings/string_view.h" namespace firebase { @@ -56,15 +59,15 @@ class DocumentKey { return [FSTDocumentKey keyWithPath:path()]; } - std::string ToString() const { - return path().CanonicalString(); - } - NSUInteger Hash() const { - return std::hash<std::string>{}(ToString()); + return util::Hash(ToString()); } #endif + std::string ToString() const { + return path().CanonicalString(); + } + /** * Creates and returns a new document key using '/' to split the string into * segments. @@ -116,7 +119,20 @@ inline bool operator>=(const DocumentKey& lhs, const DocumentKey& rhs) { return lhs.path() >= rhs.path(); } +struct DocumentKeyHash { + size_t operator()(const DocumentKey& key) const { + return util::Hash(key.path()); + } +}; + } // namespace model + +namespace util { + +template <> +struct Comparator<model::DocumentKey> : public std::less<model::DocumentKey> {}; + +} // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/Source/Model/FSTDocumentKeySet.h b/Firestore/core/src/firebase/firestore/model/document_key_set.h index 80f6624..1301bf5 100644 --- a/Firestore/Source/Model/FSTDocumentKeySet.h +++ b/Firestore/core/src/firebase/firestore/model/document_key_set.h @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google + * Copyright 2018 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,22 +14,21 @@ * limitations under the License. */ -#import <Foundation/Foundation.h> +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_SET_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_SET_H_ -#import "Firestore/third_party/Immutable/FSTImmutableSortedSet.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" -@class FSTDocumentKey; - -NS_ASSUME_NONNULL_BEGIN +namespace firebase { +namespace firestore { +namespace model { /** 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; +using DocumentKeySet = immutable::SortedSet<DocumentKey>; -@end +} // namespace model +} // namespace firestore +} // namespace firebase -NS_ASSUME_NONNULL_END +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_SET_H_ diff --git a/Firestore/core/src/firebase/firestore/model/field_mask.h b/Firestore/core/src/firebase/firestore/model/field_mask.h index b895ab3..431e05a 100644 --- a/Firestore/core/src/firebase/firestore/model/field_mask.h +++ b/Firestore/core/src/firebase/firestore/model/field_mask.h @@ -23,6 +23,7 @@ #include <vector> #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" namespace firebase { namespace firestore { @@ -55,6 +56,22 @@ class FieldMask { return fields_.end(); } + /** + * Verifies that `fieldPath` is included by at least one field in this field + * mask. + * + * This is an O(n) operation, where `n` is the size of the field mask. + */ + bool covers(const FieldPath& fieldPath) const { + for (const FieldPath& fieldMaskPath : fields_) { + if (fieldMaskPath.IsPrefixOf(fieldPath)) { + return true; + } + } + + return false; + } + std::string ToString() const { // Ideally, one should use a string builder. Since this is only non-critical // code for logging and debugging, the logic is kept simple here. @@ -70,11 +87,7 @@ class FieldMask { } NSUInteger Hash() const { - NSUInteger hashResult = 0; - for (const FieldPath& field : fields_) { - hashResult = hashResult * 31u + field.Hash(); - } - return hashResult; + return util::Hash(fields_); } #endif diff --git a/Firestore/core/src/firebase/firestore/model/field_transform.h b/Firestore/core/src/firebase/firestore/model/field_transform.h index a1dd96c..1a7127a 100644 --- a/Firestore/core/src/firebase/firestore/model/field_transform.h +++ b/Firestore/core/src/firebase/firestore/model/field_transform.h @@ -22,6 +22,7 @@ #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" namespace firebase { namespace firestore { @@ -51,9 +52,7 @@ class FieldTransform { // For Objective-C++ hash; to be removed after migration. // Do NOT use in C++ code. NSUInteger Hash() const { - NSUInteger hash = path_.Hash(); - hash = hash * 31 + transformation_->Hash(); - return hash; + return util::Hash(path_, transformation_->Hash()); } #endif // defined(__OBJC__) diff --git a/Firestore/core/src/firebase/firestore/model/precondition.h b/Firestore/core/src/firebase/firestore/model/precondition.h index 4ab03c2..b98bb45 100644 --- a/Firestore/core/src/firebase/firestore/model/precondition.h +++ b/Firestore/core/src/firebase/firestore/model/precondition.h @@ -21,7 +21,6 @@ #if defined(__OBJC__) #import "FIRTimestamp.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTDocument.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" #endif // defined(__OBJC__) diff --git a/Firestore/core/src/firebase/firestore/model/snapshot_version.h b/Firestore/core/src/firebase/firestore/model/snapshot_version.h index 1fbba1c..7ce0985 100644 --- a/Firestore/core/src/firebase/firestore/model/snapshot_version.h +++ b/Firestore/core/src/firebase/firestore/model/snapshot_version.h @@ -44,6 +44,9 @@ class SnapshotVersion { static const SnapshotVersion& None(); #if defined(__OBJC__) + SnapshotVersion() { + } + SnapshotVersion(FSTSnapshotVersion* version) // NOLINT(runtime/explicit) : timestamp_{version.timestamp.seconds, version.timestamp.nanoseconds} { } diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.cc b/Firestore/core/src/firebase/firestore/remote/serializer.cc index b5a0720..e81ea2d 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.cc +++ b/Firestore/core/src/firebase/firestore/remote/serializer.cc @@ -25,15 +25,20 @@ #include <utility> #include "Firestore/Protos/nanopb/google/firestore/v1beta1/document.pb.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" namespace firebase { namespace firestore { namespace remote { +using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldValue; using firebase::firestore::model::ObjectValue; +using firebase::firestore::model::ResourcePath; using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; namespace { @@ -43,7 +48,7 @@ class Reader; void EncodeObject(Writer* writer, const ObjectValue& object_value); -ObjectValue DecodeObject(Reader* reader); +ObjectValue::Map DecodeObject(Reader* reader); /** * Represents a nanopb tag. @@ -196,6 +201,14 @@ class Reader { return stream_.bytes_left; } + Status status() const { + return status_; + } + + void set_status(Status status) { + status_ = status; + } + private: /** * Creates a new Reader, based on the given nanopb pb_istream_t. Note that @@ -220,6 +233,8 @@ class Reader { */ uint64_t ReadVarint(); + Status status_ = Status::OK(); + pb_istream_t stream_; }; @@ -270,12 +285,17 @@ void Writer::WriteTag(Tag tag) { Tag Reader::ReadTag() { Tag tag; + if (!status_.ok()) return tag; + bool eof; - bool ok = pb_decode_tag(&stream_, &tag.wire_type, &tag.field_number, &eof); - if (!ok || eof) { - // TODO(rsgowman): figure out error handling - abort(); + if (!pb_decode_tag(&stream_, &tag.wire_type, &tag.field_number, &eof)) { + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + return tag; } + + // nanopb code always returns a false status when setting eof. + FIREBASE_ASSERT_MESSAGE(!eof, "nanopb set both ok status and eof to true"); + return tag; } @@ -301,10 +321,11 @@ void Writer::WriteVarint(uint64_t value) { * @return The decoded varint as a uint64_t. */ uint64_t Reader::ReadVarint() { - uint64_t varint_value; + if (!status_.ok()) return 0; + + uint64_t varint_value = 0; if (!pb_decode_varint(&stream_, &varint_value)) { - // TODO(rsgowman): figure out error handling - abort(); + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); } return varint_value; } @@ -315,9 +336,11 @@ void Writer::WriteNull() { void Reader::ReadNull() { uint64_t varint = ReadVarint(); + if (!status_.ok()) return; + if (varint != google_protobuf_NullValue_NULL_VALUE) { - // TODO(rsgowman): figure out error handling - abort(); + status_ = Status(FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (invalid null value)"); } } @@ -327,14 +350,18 @@ void Writer::WriteBool(bool bool_value) { bool Reader::ReadBool() { uint64_t varint = ReadVarint(); + if (!status_.ok()) return false; + switch (varint) { case 0: return false; case 1: return true; default: - // TODO(rsgowman): figure out error handling - abort(); + status_ = + Status(FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (invalid bool value)"); + return false; } } @@ -357,17 +384,21 @@ void Writer::WriteString(const std::string& string_value) { } std::string Reader::ReadString() { + if (!status_.ok()) return ""; + pb_istream_t substream; if (!pb_make_string_substream(&stream_, &substream)) { - // TODO(rsgowman): figure out error handling - abort(); + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + pb_close_string_substream(&stream_, &substream); + return ""; } std::string result(substream.bytes_left, '\0'); if (!pb_read(&substream, reinterpret_cast<pb_byte_t*>(&result[0]), substream.bytes_left)) { - // TODO(rsgowman): figure out error handling - abort(); + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + pb_close_string_substream(&stream_, &substream); + return ""; } // NB: future versions of nanopb read the remaining characters out of the @@ -375,10 +406,9 @@ std::string Reader::ReadString() { // check within pb_close_string_substream. Unfortunately, that's not present // in the current version (0.38). We'll make a stronger assertion and check // to make sure there *are* no remaining characters in the substream. - if (substream.bytes_left != 0) { - // TODO(rsgowman): figure out error handling - abort(); - } + FIREBASE_ASSERT_MESSAGE( + substream.bytes_left == 0, + "Bytes remaining in substream after supposedly reading all of them."); pb_close_string_substream(&stream_, &substream); @@ -431,29 +461,52 @@ void EncodeFieldValueImpl(Writer* writer, const FieldValue& field_value) { FieldValue DecodeFieldValueImpl(Reader* reader) { Tag tag = reader->ReadTag(); + if (!reader->status().ok()) return FieldValue::NullValue(); // Ensure the tag matches the wire type - // TODO(rsgowman): figure out error handling switch (tag.field_number) { case google_firestore_v1beta1_Value_null_value_tag: case google_firestore_v1beta1_Value_boolean_value_tag: case google_firestore_v1beta1_Value_integer_value_tag: if (tag.wire_type != PB_WT_VARINT) { - abort(); + reader->set_status( + Status(FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (mismatch between " + "the wiretype and the field number (tag))")); } break; case google_firestore_v1beta1_Value_string_value_tag: case google_firestore_v1beta1_Value_map_value_tag: if (tag.wire_type != PB_WT_STRING) { - abort(); + reader->set_status( + Status(FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (mismatch between " + "the wiretype and the field number (tag))")); } break; default: - abort(); + // We could get here for one of two reasons; either because the input + // bytes are corrupt, or because we're attempting to parse a tag that we + // haven't implemented yet. Long term, the latter reason should become + // less likely (especially in production), so we'll assume former. + + // TODO(rsgowman): While still in development, we'll contradict the above + // and assume the latter. Remove the following assertion when we're + // confident that we're handling all the tags in the protos. + FIREBASE_ASSERT_MESSAGE( + false, + "Unhandled message field number (tag): %i. (Or possibly " + "corrupt input bytes)", + tag.field_number); + reader->set_status(Status( + FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (invalid field number (tag))")); } + if (!reader->status().ok()) return FieldValue::NullValue(); + switch (tag.field_number) { case google_firestore_v1beta1_Value_null_value_tag: reader->ReadNull(); @@ -465,12 +518,15 @@ FieldValue DecodeFieldValueImpl(Reader* reader) { case google_firestore_v1beta1_Value_string_value_tag: return FieldValue::StringValue(reader->ReadString()); case google_firestore_v1beta1_Value_map_value_tag: - return FieldValue::ObjectValueFromMap( - DecodeObject(reader).internal_value); + return FieldValue::ObjectValueFromMap(DecodeObject(reader)); default: - // TODO(rsgowman): figure out error handling - abort(); + // This indicates an internal error as we've already ensured that this is + // a valid field_number. + FIREBASE_ASSERT_MESSAGE( + false, + "Somehow got an unexpected field number (tag) after verifying that " + "the field number was expected."); } } @@ -533,24 +589,34 @@ template <typename T> T Reader::ReadNestedMessage(const std::function<T(Reader*)>& read_message_fn) { // Implementation note: This is roughly modeled on pb_decode_delimited, // adjusted to account for the oneof in FieldValue. + + if (!status_.ok()) return T(); + pb_istream_t raw_substream; if (!pb_make_string_substream(&stream_, &raw_substream)) { - // TODO(rsgowman): figure out error handling - abort(); + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + pb_close_string_substream(&stream_, &raw_substream); + return T(); } Reader substream(raw_substream); + // If this fails, we *won't* return right away so that we can cleanup the + // substream (although technically, that turns out not to matter; no resource + // leaks occur if we don't do this.) + // TODO(rsgowman): Consider RAII here. (Watch out for Reader class which also + // wraps streams.) T message = read_message_fn(&substream); + status_ = substream.status(); // NB: future versions of nanopb read the remaining characters out of the // substream (and return false if that fails) as an additional safety // check within pb_close_string_substream. Unfortunately, that's not present // in the current version (0.38). We'll make a stronger assertion and check // to make sure there *are* no remaining characters in the substream. - if (substream.bytes_left() != 0) { - // TODO(rsgowman): figure out error handling - abort(); - } + FIREBASE_ASSERT_MESSAGE( + substream.bytes_left() == 0, + "Bytes remaining in substream after supposedly reading all of them."); + pb_close_string_substream(&stream_, &substream.stream_); return message; @@ -591,6 +657,7 @@ void EncodeFieldsEntry(Writer* writer, const ObjectValue::Map::value_type& kv) { ObjectValue::Map::value_type DecodeFieldsEntry(Reader* reader) { Tag tag = reader->ReadTag(); + if (!reader->status().ok()) return {}; // TODO(rsgowman): figure out error handling: We can do better than a failed // assertion. @@ -600,6 +667,7 @@ ObjectValue::Map::value_type DecodeFieldsEntry(Reader* reader) { std::string key = reader->ReadString(); tag = reader->ReadTag(); + if (!reader->status().ok()) return {}; FIREBASE_ASSERT(tag.field_number == google_firestore_v1beta1_MapValue_FieldsEntry_value_tag); FIREBASE_ASSERT(tag.wire_type == PB_WT_STRING); @@ -607,7 +675,7 @@ ObjectValue::Map::value_type DecodeFieldsEntry(Reader* reader) { FieldValue value = reader->ReadNestedMessage<FieldValue>(DecodeFieldValueImpl); - return {key, value}; + return ObjectValue::Map::value_type{key, value}; } void EncodeObject(Writer* writer, const ObjectValue& object_value) { @@ -622,12 +690,17 @@ void EncodeObject(Writer* writer, const ObjectValue& object_value) { }); } -ObjectValue DecodeObject(Reader* reader) { - ObjectValue::Map internal_value = reader->ReadNestedMessage<ObjectValue::Map>( +ObjectValue::Map DecodeObject(Reader* reader) { + if (!reader->status().ok()) return ObjectValue::Map(); + + return reader->ReadNestedMessage<ObjectValue::Map>( [](Reader* reader) -> ObjectValue::Map { ObjectValue::Map result; + if (!reader->status().ok()) return result; + while (reader->bytes_left()) { Tag tag = reader->ReadTag(); + if (!reader->status().ok()) return result; FIREBASE_ASSERT(tag.field_number == google_firestore_v1beta1_MapValue_fields_tag); FIREBASE_ASSERT(tag.wire_type == PB_WT_STRING); @@ -640,6 +713,7 @@ ObjectValue DecodeObject(Reader* reader) { // map. // TODO(rsgowman): figure out error handling: We can do better than a // failed assertion. + if (!reader->status().ok()) return result; FIREBASE_ASSERT(result.find(fv.first) == result.end()); // Add this key,fieldvalue to the results map. @@ -647,7 +721,64 @@ ObjectValue DecodeObject(Reader* reader) { } return result; }); - return ObjectValue{internal_value}; +} + +/** + * Creates the prefix for a fully qualified resource path, without a local path + * on the end. + */ +ResourcePath EncodeDatabaseId(const DatabaseId& database_id) { + return ResourcePath{"projects", database_id.project_id(), "databases", + database_id.database_id()}; +} + +/** + * Encodes a databaseId and resource path into the following form: + * /projects/$projectId/database/$databaseId/documents/$path + */ +std::string EncodeResourceName(const DatabaseId& database_id, + const ResourcePath& path) { + return EncodeDatabaseId(database_id) + .Append("documents") + .Append(path) + .CanonicalString(); +} + +/** + * Validates that a path has a prefix that looks like a valid encoded + * databaseId. + */ +bool IsValidResourceName(const ResourcePath& path) { + // Resource names have at least 4 components (project ID, database ID) + // and commonly the (root) resource type, e.g. documents + return path.size() >= 4 && path[0] == "projects" && path[2] == "databases"; +} + +/** + * Decodes a fully qualified resource name into a resource path and validates + * that there is a project and database encoded in the path. There are no + * guarantees that a local path is also encoded in this resource name. + */ +ResourcePath DecodeResourceName(absl::string_view encoded) { + ResourcePath resource = ResourcePath::FromString(encoded); + FIREBASE_ASSERT_MESSAGE(IsValidResourceName(resource), + "Tried to deserialize invalid key %s", + resource.CanonicalString().c_str()); + return resource; +} + +/** + * Decodes a fully qualified resource name into a resource path and validates + * that there is a project and database encoded in the path along with a local + * path. + */ +ResourcePath ExtractLocalPathFromResourceName( + const ResourcePath& resource_name) { + FIREBASE_ASSERT_MESSAGE( + resource_name.size() > 4 && resource_name[4] == "documents", + "Tried to deserialize invalid key %s", + resource_name.CanonicalString().c_str()); + return resource_name.PopFirst(5); } } // namespace @@ -659,9 +790,28 @@ Status Serializer::EncodeFieldValue(const FieldValue& field_value, return writer.status(); } -FieldValue Serializer::DecodeFieldValue(const uint8_t* bytes, size_t length) { +StatusOr<FieldValue> Serializer::DecodeFieldValue(const uint8_t* bytes, + size_t length) { Reader reader = Reader::Wrap(bytes, length); - return DecodeFieldValueImpl(&reader); + FieldValue fv = DecodeFieldValueImpl(&reader); + if (reader.status().ok()) { + return fv; + } else { + return reader.status(); + } +} + +std::string Serializer::EncodeKey(const DocumentKey& key) const { + return EncodeResourceName(database_id_, key.path()); +} + +DocumentKey Serializer::DecodeKey(absl::string_view name) const { + ResourcePath resource = DecodeResourceName(name); + FIREBASE_ASSERT_MESSAGE(resource[1] == database_id_.project_id(), + "Tried to deserialize key from different project."); + FIREBASE_ASSERT_MESSAGE(resource[3] == database_id_.database_id(), + "Tried to deserialize key from different database."); + return DocumentKey{ExtractLocalPathFromResourceName(resource)}; } } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.h b/Firestore/core/src/firebase/firestore/remote/serializer.h index 7f08f7d..86aa6e2 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.h +++ b/Firestore/core/src/firebase/firestore/remote/serializer.h @@ -19,12 +19,17 @@ #include <cstdint> #include <cstdlib> +#include <string> #include <vector> +#include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" #include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "absl/base/attributes.h" +#include "absl/strings/string_view.h" namespace firebase { namespace firestore { @@ -45,20 +50,13 @@ namespace remote { // interpret." Adjust for C++. class Serializer { public: - Serializer() { + /** + * @param database_id Must remain valid for the lifetime of this Serializer + * object. + */ + explicit Serializer(const firebase::firestore::model::DatabaseId& database_id) + : database_id_(database_id) { } - // TODO(rsgowman): We eventually need the DatabaseId, but can't add it just - // yet since it's not used yet (which travis complains about). So for now, - // we'll create a parameterless ctor (above) that likely won't exist in the - // final version of this class. - ///** - // * @param database_id Must remain valid for the lifetime of this Serializer - // * object. - // */ - // explicit Serializer(const firebase::firestore::model::DatabaseId& - // database_id) - // : database_id_(database_id) { - //} /** * Converts the FieldValue model passed into bytes. @@ -66,8 +64,9 @@ class Serializer { * @param field_value the model to convert. * @param[out] out_bytes A buffer to place the output. The bytes will be * appended to this vector. + * @return A Status, which if not ok(), indicates what went wrong. Note that + * errors during encoding generally indicate a serious/fatal error. */ - // TODO(rsgowman): error handling, incl return code. // TODO(rsgowman): If we never support any output except to a vector, it may // make sense to have Serializer own the vector and provide an accessor rather // than asking the user to create it first. @@ -80,26 +79,40 @@ class Serializer { * * @param bytes The bytes to convert. It's assumed that exactly all of the * bytes will be used by this conversion. - * @return The model equivalent of the bytes. + * @return The model equivalent of the bytes or a Status indicating + * what went wrong. */ - // TODO(rsgowman): error handling. - model::FieldValue DecodeFieldValue(const uint8_t* bytes, size_t length); + util::StatusOr<model::FieldValue> DecodeFieldValue(const uint8_t* bytes, + size_t length); /** * @brief Converts from bytes to the model FieldValue format. * * @param bytes The bytes to convert. It's assumed that exactly all of the * bytes will be used by this conversion. - * @return The model equivalent of the bytes. + * @return The model equivalent of the bytes or a Status indicating + * what went wrong. */ - // TODO(rsgowman): error handling. - model::FieldValue DecodeFieldValue(const std::vector<uint8_t>& bytes) { + util::StatusOr<model::FieldValue> DecodeFieldValue( + const std::vector<uint8_t>& bytes) { return DecodeFieldValue(bytes.data(), bytes.size()); } + /** + * Encodes the given document key as a fully qualified name. This includes the + * databaseId associated with this Serializer and the key path. + */ + std::string EncodeKey( + const firebase::firestore::model::DocumentKey& key) const; + + /** + * Decodes the given document key from a fully qualified name. + */ + firebase::firestore::model::DocumentKey DecodeKey( + absl::string_view name) const; + private: - // TODO(rsgowman): We don't need the database_id_ yet (but will eventually). - // model::DatabaseId* database_id_; + const firebase::firestore::model::DatabaseId& database_id_; }; } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt index 3afead1..29d91c7 100644 --- a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt @@ -109,6 +109,63 @@ else() endif() +## async queue + +check_symbol_exists(dispatch_async_f dispatch/dispatch.h HAVE_LIBDISPATCH) + +cc_library( + firebase_firestore_util_executor_std + SOURCES + executor_std.cc + executor_std.h + executor.h + DEPENDS + absl_bad_optional_access + absl_optional + ${FIREBASE_FIRESTORE_UTIL_LOG} + EXCLUDE_FROM_ALL +) + +if(HAVE_LIBDISPATCH) +cc_library( + firebase_firestore_util_executor_libdispatch + SOURCES + executor_libdispatch.cc + executor_libdispatch.h + executor.h + DEPENDS + absl_bad_optional_access + absl_optional + absl_strings + ${FIREBASE_FIRESTORE_UTIL_LOG} + EXCLUDE_FROM_ALL +) +endif() + +if(HAVE_LIBDISPATCH) + set( + FIREBASE_FIRESTORE_UTIL_EXECUTOR + firebase_firestore_util_executor_libdispatch + ) + +else() + set( + FIREBASE_FIRESTORE_UTIL_EXECUTOR + firebase_firestore_util_executor_std + ) + +endif() + +cc_library( + firebase_firestore_util_async_queue + SOURCES + async_queue.cc + async_queue.h + DEPENDS + ${FIREBASE_FIRESTORE_UTIL_EXECUTOR} + ${FIREBASE_FIRESTORE_UTIL_LOG} + EXCLUDE_FROM_ALL +) ## main library @@ -128,6 +185,7 @@ cc_library( comparison.cc comparison.h config.h + hashing.h iterator_adaptors.h ordered_code.cc ordered_code.h @@ -143,6 +201,7 @@ cc_library( DEPENDS absl_base firebase_firestore_util_base + firebase_firestore_util_async_queue ${FIREBASE_FIRESTORE_UTIL_LOG} ${FIREBASE_FIRESTORE_UTIL_RANDOM} ) diff --git a/Firestore/core/src/firebase/firestore/util/async_queue.cc b/Firestore/core/src/firebase/firestore/util/async_queue.cc new file mode 100644 index 0000000..71f5cc5 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/async_queue.cc @@ -0,0 +1,140 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/src/firebase/firestore/util/async_queue.h" + +#include <utility> + +#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "absl/memory/memory.h" + +namespace firebase { +namespace firestore { +namespace util { + +using internal::Executor; + +AsyncQueue::AsyncQueue(std::unique_ptr<Executor> executor) + : executor_{std::move(executor)} { + is_operation_in_progress_ = false; +} + +void AsyncQueue::VerifyIsCurrentExecutor() const { + FIREBASE_ASSERT_MESSAGE( + executor_->IsCurrentExecutor(), + "Expected to be called by the executor associated with this queue " + "(expected executor: '%s', actual executor: '%s')", + executor_->Name().c_str(), executor_->CurrentExecutorName().c_str()); +} + +void AsyncQueue::VerifyIsCurrentQueue() const { + VerifyIsCurrentExecutor(); + FIREBASE_ASSERT_MESSAGE( + is_operation_in_progress_, + "VerifyIsCurrentQueue called when no operation is executing " + "(expected executor: '%s', actual executor: '%s')", + executor_->Name().c_str(), executor_->CurrentExecutorName().c_str()); +} + +void AsyncQueue::ExecuteBlocking(const Operation& operation) { + VerifyIsCurrentExecutor(); + FIREBASE_ASSERT_MESSAGE(!is_operation_in_progress_, + "ExecuteBlocking may not be called " + "before the previous operation finishes executing"); + + is_operation_in_progress_ = true; + operation(); + is_operation_in_progress_ = false; +} + +void AsyncQueue::Enqueue(const Operation& operation) { + VerifySequentialOrder(); + EnqueueRelaxed(operation); +} + +void AsyncQueue::EnqueueRelaxed(const Operation& operation) { + executor_->Execute(Wrap(operation)); +} + +DelayedOperation AsyncQueue::EnqueueAfterDelay(const Milliseconds delay, + const TimerId timer_id, + const Operation& operation) { + VerifyIsCurrentExecutor(); + + // While not necessarily harmful, we currently don't expect to have multiple + // callbacks with the same timer_id in the queue, so defensively reject + // them. + FIREBASE_ASSERT_MESSAGE( + !IsScheduled(timer_id), + "Attempted to schedule multiple operations with id %d", timer_id); + + Executor::TaggedOperation tagged{static_cast<int>(timer_id), Wrap(operation)}; + return executor_->Schedule(delay, std::move(tagged)); +} + +AsyncQueue::Operation AsyncQueue::Wrap(const Operation& operation) { + // Decorator pattern: wrap `operation` into a call to `ExecuteBlocking` to + // ensure that it doesn't spawn any nested operations. + + // Note: can't move `operation` into lambda until C++14. + return [this, operation] { ExecuteBlocking(operation); }; +} + +void AsyncQueue::VerifySequentialOrder() const { + // This is the inverse of `VerifyIsCurrentQueue`. + FIREBASE_ASSERT_MESSAGE( + !is_operation_in_progress_ || !executor_->IsCurrentExecutor(), + "Enforcing sequential order failed: currently executing operations " + "cannot enqueue more operations " + "(this queue's executor: '%s', current executor: '%s')", + executor_->Name().c_str(), executor_->CurrentExecutorName().c_str()); +} + +// Test-only functions + +void AsyncQueue::EnqueueBlocking(const Operation& operation) { + VerifySequentialOrder(); + executor_->ExecuteBlocking(Wrap(operation)); +} + +bool AsyncQueue::IsScheduled(const TimerId timer_id) const { + return executor_->IsScheduled(static_cast<int>(timer_id)); +} + +void AsyncQueue::RunScheduledOperationsUntil(const TimerId last_timer_id) { + FIREBASE_ASSERT_MESSAGE( + !executor_->IsCurrentExecutor(), + "RunScheduledOperationsUntil must not be called on the queue"); + + executor_->ExecuteBlocking([this, last_timer_id] { + FIREBASE_ASSERT_MESSAGE( + last_timer_id == TimerId::All || IsScheduled(last_timer_id), + "Attempted to run scheduled operations until missing timer id: %d", + last_timer_id); + + for (auto next = executor_->PopFromSchedule(); next.has_value(); + next = executor_->PopFromSchedule()) { + next->operation(); + if (next->tag == static_cast<int>(last_timer_id)) { + break; + } + } + }); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/async_queue.h b/Firestore/core/src/firebase/firestore/util/async_queue.h new file mode 100644 index 0000000..e2df387 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/async_queue.h @@ -0,0 +1,164 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_H_ + +#include <atomic> +#include <chrono> // NOLINT(build/c++11) +#include <functional> +#include <memory> + +#include "Firestore/core/src/firebase/firestore/util/executor.h" + +namespace firebase { +namespace firestore { +namespace util { + +/** + * Well-known "timer" ids used when scheduling delayed operations on the + * AsyncQueue. These ids can then be used from tests to check for the + * presence of delayed operations or to run them early. + */ +enum class TimerId { + /** All can be used with `RunDelayedOperationsUntil` to run all timers. */ + All, + + /** + * The following 4 timers are used in `Stream` for the listen and write + * streams. The "Idle" timer is used to close the stream due to inactivity. + * The "ConnectionBackoff" timer is used to restart a stream once the + * appropriate backoff delay has elapsed. + */ + ListenStreamIdle, + ListenStreamConnectionBackoff, + WriteStreamIdle, + WriteStreamConnectionBackoff, + + /** + * A timer used in `OnlineStateTracker` to transition from + * `OnlineStateUnknown` to `Offline` after a set timeout, rather than waiting + * indefinitely for success or failure. + */ + OnlineStateTimeout, +}; + +// A serial queue that executes given operations asynchronously, one at a time. +// Operations may be scheduled to be executed as soon as possible or in the +// future. Operations scheduled for the same time are FIFO-ordered. +// +// `AsyncQueue` wraps a platform-specific executor, adding checks that enforce +// sequential ordering of operations: an enqueued operation, while being run, +// normally cannot enqueue other operations for immediate execution (but see +// `EnqueueRelaxed`). +// +// `AsyncQueue` methods have particular expectations about whether they must be +// invoked on the queue or not; check "preconditions" section in comments on +// each method. +// +// A significant portion of `AsyncQueue` interface only exists for test purposes +// and must *not* be used in regular code. +class AsyncQueue { + public: + using Operation = internal::Executor::Operation; + using Milliseconds = internal::Executor::Milliseconds; + + explicit AsyncQueue(std::unique_ptr<internal::Executor> executor); + + // Asserts for the caller that it is being invoked as part of an operation on + // the `AsyncQueue`. + void VerifyIsCurrentQueue() const; + + // Enqueue methods + + // Puts the `operation` on the queue to be executed as soon as possible, while + // maintaining FIFO order. + // + // Precondition: `Enqueue` calls cannot be nested; that is, `Enqueue` may not + // be called by a previously enqueued operation when it is run (as a special + // case, destructors invoked when an enqueued operation has run and is being + // destroyed may invoke `Enqueue`). + void Enqueue(const Operation& operation); + + // Like `Enqueue`, but without applying any prerequisite checks. + void EnqueueRelaxed(const Operation& operation); + + // Puts the `operation` on the queue to be executed `delay` milliseconds from + // now, and returns a handle that allows to cancel the operation (provided it + // hasn't run already). + // + // `operation` is tagged by a `timer_id` which allows to identify the caller. + // Only one operation tagged with any given `timer_id` may be on the queue at + // any time; an attempt to put another such operation will result in an + // assertion failure. In tests, these tags also allow to check for presence of + // certain operations and to run certain operations in advance. + // + // Precondition: `EnqueueAfterDelay` is being invoked asynchronously on the + // queue. + DelayedOperation EnqueueAfterDelay(Milliseconds delay, + TimerId timer_id, + const Operation& operation); + + // Direct execution + + // Immediately executes the `operation` on the queue. + // + // This is largely a workaround to allow other classes (GRPC) to directly + // access the underlying dispatch queue without getting `AsyncQueue` into an + // inconsistent state. + // + // Precondition: no other operation is being executed on the queue at the + // moment of the call (i.e., `ExecuteBlocking` cannot call `ExecuteBlocking`). + // + // Precondition: `ExecuteBlocking` is being invoked asynchronously on the + // queue. + void ExecuteBlocking(const Operation& operation); + + // Test-only interface follows + // TODO(varconst): move the test-only interface into a helper object that is + // a friend of AsyncQueue and delegates its public methods to private methods + // on AsyncQueue. + + // Like `Enqueue`, but blocks until the `operation` is complete. + void EnqueueBlocking(const Operation& operation); + + // Checks whether an operation tagged with `timer_id` is currently scheduled + // for execution in the future. + bool IsScheduled(TimerId timer_id) const; + + // Force runs operations scheduled for future execution, in scheduled order, + // up to *and including* the operation tagged with `last_timer_id`. + // + // Precondition: `RunScheduledOperationsUntil` is *not* being invoked on the + // queue. + void RunScheduledOperationsUntil(TimerId last_timer_id); + + private: + Operation Wrap(const Operation& operation); + + // Asserts that the current invocation happens asynchronously on the queue. + void VerifyIsCurrentExecutor() const; + void VerifySequentialOrder() const; + + std::atomic<bool> is_operation_in_progress_; + std::unique_ptr<internal::Executor> executor_; +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_H_ diff --git a/Firestore/core/src/firebase/firestore/util/executor.h b/Firestore/core/src/firebase/firestore/util/executor.h new file mode 100644 index 0000000..df8b0b5 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor.h @@ -0,0 +1,129 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_H_ + +#include <chrono> // NOLINT(build/c++11) +#include <functional> +#include <string> +#include <utility> + +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace util { + +// A handle to an operation scheduled for future execution. The handle may +// outlive the operation, but it *cannot* outlive the executor that created it. +class DelayedOperation { + public: + DelayedOperation() { + } + + // If the operation has not been run yet, cancels the operation. Otherwise, + // this function is a no-op. + void Cancel() { + cancel_func_(); + } + + // Internal use only. + explicit DelayedOperation(std::function<void()>&& cancel_func) + : cancel_func_{std::move(cancel_func)} { + } + + private: + std::function<void()> cancel_func_; +}; + +namespace internal { + +// An interface to a platform-specific executor of asynchronous operations +// (called tasks on other platforms). +// +// Operations may be scheduled for immediate or delayed execution. Operations +// delayed until the exact same time are scheduled in FIFO order. +// +// The operations are executed sequentially; only a single operation is executed +// at any given time. +// +// Delayed operations may be canceled if they have not already been run. +class Executor { + public: + using Tag = int; + using Operation = std::function<void()>; + using Milliseconds = std::chrono::milliseconds; + + // Operations scheduled for future execution have an opaque tag. The value of + // the tag is ignored by the executor but can be used to find operations with + // a given tag after they are scheduled. + struct TaggedOperation { + TaggedOperation() { + } + TaggedOperation(const Tag tag, Operation&& operation) + : tag{tag}, operation{std::move(operation)} { + } + Tag tag = 0; + Operation operation; + }; + + virtual ~Executor() { + } + + // Schedules the `operation` to be asynchronously executed as soon as + // possible, in FIFO order. + virtual void Execute(Operation&& operation) = 0; + // Like `Execute`, but blocks until the `operation` finishes, consequently + // draining immediate operations from the executor. + virtual void ExecuteBlocking(Operation&& operation) = 0; + // Scheduled the given `operation` to be executed after `delay` milliseconds + // from now, and returns a handle that allows to cancel the operation + // (provided it hasn't been run already). The operation is tagged to allow + // retrieving it later. + // + // `delay` must be non-negative; use `Execute` to schedule operations for + // immediate execution. + virtual DelayedOperation Schedule(Milliseconds delay, + TaggedOperation&& operation) = 0; + + // Checks for the caller whether it is being invoked by this executor. + virtual bool IsCurrentExecutor() const = 0; + // Returns some sort of an identifier for the current execution context. The + // only guarantee is that it will return different values depending on whether + // this function is invoked by this executor or not. + virtual std::string CurrentExecutorName() const = 0; + // Like `CurrentExecutorName`, but returns an identifier for this executor, + // whether the caller code currently runs on this executor or not. + virtual std::string Name() const = 0; + + // Checks whether an operation tagged with the given `tag` is currently + // scheduled for future execution. + virtual bool IsScheduled(Tag tag) const = 0; + // Removes the nearest due scheduled operation from the schedule and returns + // it to the caller. This function may be used to reschedule operations. + // Immediate operations don't count; only operations scheduled for delayed + // execution may be removed. If no such operations are currently scheduled, an + // empty `optional` is returned. + virtual absl::optional<TaggedOperation> PopFromSchedule() = 0; +}; + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_H_ diff --git a/Firestore/core/src/firebase/firestore/util/executor_libdispatch.cc b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.cc new file mode 100644 index 0000000..b40f0dd --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.cc @@ -0,0 +1,296 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" + +namespace firebase { +namespace firestore { +namespace util { +namespace internal { + +namespace { + +absl::string_view StringViewFromDispatchLabel(const char* const label) { + // Make sure string_view's data is not null, because it's used for logging. + return label ? absl::string_view{label} : absl::string_view{""}; +} + +void RunSynchronized(const ExecutorLibdispatch* const executor, + std::function<void()>&& work) { + if (executor->IsCurrentExecutor()) { + work(); + } else { + DispatchSync(executor->dispatch_queue(), std::move(work)); + } +} + +} // namespace + +void DispatchAsync(const dispatch_queue_t queue, std::function<void()>&& work) { + // Dynamically allocate the function to make sure the object is valid by the + // time libdispatch gets to it. + const auto wrap = new std::function<void()>{std::move(work)}; + + dispatch_async_f(queue, wrap, [](void* const raw_work) { + const auto unwrap = static_cast<std::function<void()>*>(raw_work); + (*unwrap)(); + delete unwrap; + }); +} + +void DispatchSync(const dispatch_queue_t queue, Executor::Operation work) { + // Unlike dispatch_async_f, dispatch_sync_f blocks until the work passed to it + // is done, so passing a reference to a local variable is okay. + dispatch_sync_f(queue, &work, [](void* const raw_work) { + const auto unwrap = static_cast<const Executor::Operation*>(raw_work); + (*unwrap)(); + }); +} + +// Represents a "busy" time slot on the schedule. +// +// Since libdispatch doesn't provide a way to cancel a scheduled operation, once +// a slot is created, it will always stay in the schedule until the time is +// past. Consequently, it is more useful to think of a time slot than +// a particular scheduled operation -- by the time the slot comes, operation may +// or may not be there (imagine getting to a meeting and finding out it's been +// canceled). +// +// Precondition: all member functions, including the constructor, are *only* +// invoked on the Firestore queue. +// +// Ownership: +// +// - `TimeSlot` is exclusively owned by libdispatch; +// - `ExecutorLibdispatch` contains non-owning pointers to `TimeSlot`s; +// - invariant: if the executor contains a pointer to a `TimeSlot`, it is +// a valid object. It is achieved because when libdispatch invokes +// a `TimeSlot`, it always removes it from the executor before deleting it. +// The reverse is not true: a canceled time slot is removed from the executor, +// but won't be destroyed until its original due time is past. + +class TimeSlot { + public: + TimeSlot(ExecutorLibdispatch* executor, + Executor::Milliseconds delay, + Executor::TaggedOperation&& operation); + + // Returns the operation that was scheduled for this time slot and turns the + // slot into a no-op. + Executor::TaggedOperation Unschedule(); + + bool operator<(const TimeSlot& rhs) const { + return target_time_ < rhs.target_time_; + } + bool operator==(const Executor::Tag tag) const { + return tagged_.tag == tag; + } + + void MarkDone() { + done_ = true; + } + + static void InvokedByLibdispatch(void* const raw_self); + + private: + void Execute(); + void RemoveFromSchedule(); + + using TimePoint = std::chrono::time_point<std::chrono::system_clock, + Executor::Milliseconds>; + + ExecutorLibdispatch* const executor_; + const TimePoint target_time_; // Used for sorting + Executor::TaggedOperation tagged_; + + // True if the operation has either been run or canceled. + // + // Note on thread-safety: because the precondition is that all member + // functions of this class are executed on the dispatch queue, no + // synchronization is required for `done_`. + bool done_ = false; +}; + +TimeSlot::TimeSlot(ExecutorLibdispatch* const executor, + const Executor::Milliseconds delay, + Executor::TaggedOperation&& operation) + : executor_{executor}, + target_time_{std::chrono::time_point_cast<Executor::Milliseconds>( + std::chrono::system_clock::now()) + + delay}, + tagged_{std::move(operation)} { +} + +Executor::TaggedOperation TimeSlot::Unschedule() { + if (!done_) { + RemoveFromSchedule(); + } + return std::move(tagged_); +} + +void TimeSlot::InvokedByLibdispatch(void* const raw_self) { + auto const self = static_cast<TimeSlot*>(raw_self); + self->Execute(); + delete self; +} + +void TimeSlot::Execute() { + if (done_) { + // `done_` might mean that the executor is already destroyed, so don't call + // `RemoveFromSchedule`. + return; + } + + RemoveFromSchedule(); + + FIREBASE_ASSERT_MESSAGE(tagged_.operation, + "TimeSlot contains an invalid function object"); + tagged_.operation(); +} + +void TimeSlot::RemoveFromSchedule() { + executor_->RemoveFromSchedule(this); +} + +// ExecutorLibdispatch + +ExecutorLibdispatch::ExecutorLibdispatch(const dispatch_queue_t dispatch_queue) + : dispatch_queue_{dispatch_queue} { +} +ExecutorLibdispatch::ExecutorLibdispatch() + : ExecutorLibdispatch{dispatch_queue_create("com.google.firebase.firestore", + DISPATCH_QUEUE_SERIAL)} { +} + +ExecutorLibdispatch::~ExecutorLibdispatch() { + // Turn any operations that might still be in the queue into no-ops, lest + // they try to access `ExecutorLibdispatch` after it gets destroyed. Because + // the queue is serial, by the time libdispatch gets to the newly-enqueued + // work, the pending operations that might have been in progress would have + // already finished. + RunSynchronized(this, [this] { + for (auto slot : schedule_) { + slot->MarkDone(); + } + }); +} + +bool ExecutorLibdispatch::IsCurrentExecutor() const { + return GetCurrentQueueLabel().data() == GetTargetQueueLabel().data(); +} +std::string ExecutorLibdispatch::CurrentExecutorName() const { + return GetCurrentQueueLabel().data(); +} +std::string ExecutorLibdispatch::Name() const { + return GetTargetQueueLabel().data(); +} + +void ExecutorLibdispatch::Execute(Operation&& operation) { + DispatchAsync(dispatch_queue(), std::move(operation)); +} +void ExecutorLibdispatch::ExecuteBlocking(Operation&& operation) { + DispatchSync(dispatch_queue(), std::move(operation)); +} + +DelayedOperation ExecutorLibdispatch::Schedule(const Milliseconds delay, + TaggedOperation&& operation) { + namespace chr = std::chrono; + const dispatch_time_t delay_ns = dispatch_time( + DISPATCH_TIME_NOW, chr::duration_cast<chr::nanoseconds>(delay).count()); + + // Ownership is fully transferred to libdispatch -- because it's impossible + // to truly cancel work after it's been dispatched, libdispatch is + // guaranteed to outlive the executor, and it's possible for work to be + // invoked by libdispatch after the executor is destroyed. Executor only + // stores an observer pointer to the operation. + + auto const time_slot = new TimeSlot{this, delay, std::move(operation)}; + dispatch_after_f(delay_ns, dispatch_queue(), time_slot, + TimeSlot::InvokedByLibdispatch); + RunSynchronized(this, [this, time_slot] { schedule_.push_back(time_slot); }); + return DelayedOperation{[this, time_slot] { + // `time_slot` might be destroyed by the time cancellation function runs. + // Therefore, don't access any methods on `time_slot`, only use it as + // a handle to remove from `schedule_`. + RemoveFromSchedule(time_slot); + }}; +} + +void ExecutorLibdispatch::RemoveFromSchedule(const TimeSlot* const to_remove) { + RunSynchronized(this, [this, to_remove] { + const auto found = std::find_if( + schedule_.begin(), schedule_.end(), + [to_remove](const TimeSlot* op) { return op == to_remove; }); + // It's possible for the operation to be missing if libdispatch gets to run + // it after it was force-run, for example. + if (found != schedule_.end()) { + (*found)->MarkDone(); + schedule_.erase(found); + } + }); +} + +// GetLabel functions are guaranteed to never return a "null" string_view +// (i.e. data() != nullptr). +absl::string_view ExecutorLibdispatch::GetCurrentQueueLabel() const { + // Note: dispatch_queue_get_label may return nullptr if the queue wasn't + // initialized with a label. + return StringViewFromDispatchLabel( + dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)); +} + +absl::string_view ExecutorLibdispatch::GetTargetQueueLabel() const { + return StringViewFromDispatchLabel( + dispatch_queue_get_label(dispatch_queue())); +} + +// Test-only methods + +bool ExecutorLibdispatch::IsScheduled(const Tag tag) const { + bool result = false; + RunSynchronized(this, [this, tag, &result] { + result = std::find_if(schedule_.begin(), schedule_.end(), + [&tag](const TimeSlot* const operation) { + return *operation == tag; + }) != schedule_.end(); + }); + return result; +} + +absl::optional<Executor::TaggedOperation> +ExecutorLibdispatch::PopFromSchedule() { + absl::optional<Executor::TaggedOperation> result; + + RunSynchronized(this, [this, &result] { + if (schedule_.empty()) { + return; + } + // Sorting upon each call to `PopFromSchedule` is inefficient, which is + // consciously ignored because this function is only ever called from tests. + std::sort( + schedule_.begin(), schedule_.end(), + [](const TimeSlot* lhs, const TimeSlot* rhs) { return *lhs < *rhs; }); + const auto nearest = schedule_.begin(); + result = (*nearest)->Unschedule(); + }); + + return result; +} + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/executor_libdispatch.h b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.h new file mode 100644 index 0000000..b32dbff --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.h @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_LIBDISPATCH_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_LIBDISPATCH_H_ + +#include <atomic> +#include <chrono> // NOLINT(build/c++11) +#include <functional> +#include <memory> +#include <string> +#include <utility> +#include <vector> +#include "dispatch/dispatch.h" + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "absl/strings/string_view.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace internal { + +// Generic wrapper over `dispatch_async_f`, providing `dispatch_async`-like +// interface: accepts an arbitrary invocable object in place of an Objective-C +// block. +void DispatchAsync(const dispatch_queue_t queue, std::function<void()>&& work); + +// Similar to `DispatchAsync` but wraps `dispatch_sync_f`. +void DispatchSync(const dispatch_queue_t queue, std::function<void()> work); + +class TimeSlot; + +// A serial queue built on top of libdispatch. The operations are run on +// a dedicated serial dispatch queue. +class ExecutorLibdispatch : public Executor { + public: + ExecutorLibdispatch(); + explicit ExecutorLibdispatch(dispatch_queue_t dispatch_queue); + ~ExecutorLibdispatch(); + + bool IsCurrentExecutor() const override; + std::string CurrentExecutorName() const override; + std::string Name() const override; + + void Execute(Operation&& operation) override; + void ExecuteBlocking(Operation&& operation) override; + DelayedOperation Schedule(Milliseconds delay, + TaggedOperation&& operation) override; + + void RemoveFromSchedule(const TimeSlot* to_remove); + + bool IsScheduled(Tag tag) const override; + absl::optional<TaggedOperation> PopFromSchedule() override; + + dispatch_queue_t dispatch_queue() const { + return dispatch_queue_; + } + + private: + // GetLabel functions are guaranteed to never return a "null" string_view + // (i.e. data() != nullptr). + absl::string_view GetCurrentQueueLabel() const; + absl::string_view GetTargetQueueLabel() const; + + std::atomic<dispatch_queue_t> dispatch_queue_; + // Stores non-owned pointers to `TimeSlot`s. + // Invariant: if a `TimeSlot` is in `schedule_`, it's a valid pointer. + std::vector<TimeSlot*> schedule_; +}; + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_LIBDISPATCH_H_ diff --git a/Firestore/core/src/firebase/firestore/util/executor_std.cc b/Firestore/core/src/firebase/firestore/util/executor_std.cc new file mode 100644 index 0000000..59197e1 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor_std.cc @@ -0,0 +1,155 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/src/firebase/firestore/util/executor_std.h" + +#include <future> // NOLINT(build/c++11) +#include <sstream> + +namespace firebase { +namespace firestore { +namespace util { +namespace internal { + +namespace { + +// The only guarantee is that different `thread_id`s will produce different +// values. +std::string ThreadIdToString(const std::thread::id thread_id) { + std::ostringstream stream; + stream << thread_id; + return stream.str(); +} + +} // namespace + +ExecutorStd::ExecutorStd() { + // Somewhat counter-intuitively, constructor of `std::atomic` assigns the + // value non-atomically, so the atomic initialization must be provided here, + // before the worker thread is started. + // See [this thread](https://stackoverflow.com/questions/25609858) for context + // on the constructor. + current_id_ = 0; + shutting_down_ = false; + worker_thread_ = std::thread{&ExecutorStd::PollingThread, this}; +} + +ExecutorStd::~ExecutorStd() { + shutting_down_ = true; + // Make sure the worker thread is not blocked, so that the call to `join` + // doesn't hang. + UnblockQueue(); + worker_thread_.join(); +} + +void ExecutorStd::Execute(Operation&& operation) { + PushOnSchedule(std::move(operation), Immediate()); +} + +DelayedOperation ExecutorStd::Schedule(const Milliseconds delay, + TaggedOperation&& tagged) { + // While negative delay can be interpreted as a request for immediate + // execution, supporting it would provide a hacky way to modify FIFO ordering + // of immediate operations. + FIREBASE_ASSERT_MESSAGE(delay.count() >= 0, + "Schedule: delay cannot be negative"); + + namespace chr = std::chrono; + const auto now = chr::time_point_cast<Milliseconds>(chr::system_clock::now()); + const auto id = + PushOnSchedule(std::move(tagged.operation), now + delay, tagged.tag); + + return DelayedOperation{[this, id] { TryCancel(id); }}; +} + +void ExecutorStd::TryCancel(const Id operation_id) { + schedule_.RemoveIf( + [operation_id](const Entry& e) { return e.id == operation_id; }); +} + +ExecutorStd::Id ExecutorStd::PushOnSchedule(Operation&& operation, + const TimePoint when, + const Tag tag) { + // Note: operations scheduled for immediate execution don't actually need an + // id. This could be tweaked to reuse the same id for all such operations. + const auto id = NextId(); + schedule_.Push(Entry{std::move(operation), id, tag}, when); + return id; +} + +void ExecutorStd::PollingThread() { + while (!shutting_down_) { + Entry entry = schedule_.PopBlocking(); + if (entry.tagged.operation) { + entry.tagged.operation(); + } + } +} + +void ExecutorStd::UnblockQueue() { + // Put a no-op for immediate execution on the queue to ensure that + // `schedule_.PopBlocking` returns, and worker thread can notice that shutdown + // is in progress. + schedule_.Push(Entry{[] {}, /*id=*/0}, Immediate()); +} + +ExecutorStd::Id ExecutorStd::NextId() { + // The wrap around after ~4 billion operations is explicitly ignored. Even if + // an instance of `ExecutorStd` runs long enough to get `current_id_` to + // overflow, it's extremely unlikely that any object still holds a reference + // that is old enough to cause a conflict. + return current_id_++; +} + +bool ExecutorStd::IsCurrentExecutor() const { + return std::this_thread::get_id() == worker_thread_.get_id(); +} + +std::string ExecutorStd::CurrentExecutorName() const { + return ThreadIdToString(std::this_thread::get_id()); +} + +std::string ExecutorStd::Name() const { + return ThreadIdToString(worker_thread_.get_id()); +} + +void ExecutorStd::ExecuteBlocking(Operation&& operation) { + std::promise<void> signal_finished; + Execute([&] { + operation(); + signal_finished.set_value(); + }); + signal_finished.get_future().wait(); +} + +bool ExecutorStd::IsScheduled(const Tag tag) const { + return schedule_.Contains( + [&tag](const Entry& e) { return e.tagged.tag == tag; }); +} + +absl::optional<Executor::TaggedOperation> ExecutorStd::PopFromSchedule() { + auto removed = + schedule_.RemoveIf([](const Entry& e) { return !e.IsImmediate(); }); + if (!removed.has_value()) { + return {}; + } + return {std::move(removed.value().tagged)}; +} + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/executor_std.h b/Firestore/core/src/firebase/firestore/util/executor_std.h new file mode 100644 index 0000000..4ac62e1 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor_std.h @@ -0,0 +1,281 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_STD_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_STD_H_ + +#include <algorithm> +#include <atomic> +#include <condition_variable> // NOLINT(build/c++11) +#include <deque> +#include <mutex> // NOLINT(build/c++11) +#include <string> +#include <thread> // NOLINT(build/c++11) +#include <utility> + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace async { + +// A thread-safe class similar to a priority queue where the entries are +// prioritized by the time for which they're scheduled. Entries scheduled for +// the exact same time are prioritized in FIFO order. +// +// The main function of `Schedule` is `PopBlocking`, which sleeps until an entry +// becomes available. It correctly handles entries being asynchonously added or +// removed from the schedule. +// +// The details of time management are completely concealed within the class. +// Once an entry is scheduled, there is no way to reschedule or even retrieve +// the time. +template <typename T> +class Schedule { + // Internal invariants: + // - entries are always in sorted order, leftmost entry is always the most + // due; + // - each operation modifying the queue notifies the condition variable `cv_`. + public: + using Duration = std::chrono::milliseconds; + using Clock = std::chrono::system_clock; + // Entries are scheduled using absolute time. + using TimePoint = std::chrono::time_point<Clock, Duration>; + + // Schedules an entry for the specified time due. `due` may be in the past. + void Push(const T& value, const TimePoint due) { + InsertPreservingOrder(Entry{value, due}); + } + void Push(T&& value, const TimePoint due) { + InsertPreservingOrder(Entry{std::move(value), due}); + } + + // If the queue contains at least one entry for which the scheduled time is + // due now (according to the system clock), removes the entry which is the + // most overdue from the queue and returns it. If no entry is due, returns an + // empty `optional`. + absl::optional<T> PopIfDue() { + std::lock_guard<std::mutex> lock{mutex_}; + + if (HasDueLocked()) { + return ExtractLocked(scheduled_.begin()); + } + return {}; + } + + // Blocks until at least one entry is available for which the scheduled time + // is due now (according to the system clock), removes the entry which is the + // most overdue from the queue and returns it. The function will + // attempt to minimize both the waiting time and busy waiting. + T PopBlocking() { + std::unique_lock<std::mutex> lock{mutex_}; + + while (true) { + cv_.wait(lock, [this] { return !scheduled_.empty(); }); + + // To minimize busy waiting, sleep until either the nearest entry in the + // future either changes, or else becomes due. + const auto until = scheduled_.front().due; + cv_.wait_until(lock, until, + [this, until] { return scheduled_.front().due != until; }); + // There are 3 possibilities why `wait_until` has returned: + // - `wait_until` has timed out, in which case the current time is at + // least `until`, so there must be an overdue entry; + // - a new entry has been added which comes before `until`. It must be + // either overdue (in which case `HasDueLocked` will break the cycle), + // or else `until` must be reevaluated (on the next iteration of the + // loop); + // - `until` entry has been removed. This means `until` has to be + // reevaluated, similar to #2. + + if (HasDueLocked()) { + return ExtractLocked(scheduled_.begin()); + } + } + } + + bool empty() const { + std::lock_guard<std::mutex> lock{mutex_}; + return scheduled_.empty(); + } + + size_t size() const { + std::lock_guard<std::mutex> lock{mutex_}; + return scheduled_.size(); + } + + // Removes the first entry satisfying predicate from the queue and returns it. + // If no such entry exists, returns an empty `optional`. Predicate is applied + // to entries in order according to their scheduled time. + // + // Note that this function doesn't take into account whether the removed entry + // is past its due time. + template <typename Pred> + absl::optional<T> RemoveIf(const Pred pred) { + std::lock_guard<std::mutex> lock{mutex_}; + + for (auto iter = scheduled_.begin(), end = scheduled_.end(); iter != end; + ++iter) { + if (pred(iter->value)) { + return ExtractLocked(iter); + } + } + return {}; + } + + // Checks whether the queue contains an entry satisfying the given predicate. + template <typename Pred> + bool Contains(const Pred pred) const { + std::lock_guard<std::mutex> lock{mutex_}; + return std::any_of(scheduled_.begin(), scheduled_.end(), + [&pred](const Entry& s) { return pred(s.value); }); + } + + private: + struct Entry { + bool operator<(const Entry& rhs) const { + return due < rhs.due; + } + + T value; + TimePoint due; + }; + // All removals are on the front, but most insertions are expected to be on + // the back. + using Container = std::deque<Entry>; + using Iterator = typename Container::iterator; + + void InsertPreservingOrder(Entry&& new_entry) { + std::lock_guard<std::mutex> lock{mutex_}; + + const auto insertion_point = + std::upper_bound(scheduled_.begin(), scheduled_.end(), new_entry); + scheduled_.insert(insertion_point, std::move(new_entry)); + + cv_.notify_one(); + } + + // This function expects the mutex to be already locked. + bool HasDueLocked() const { + namespace chr = std::chrono; + const auto now = chr::time_point_cast<Duration>(Clock::now()); + return !scheduled_.empty() && now >= scheduled_.front().due; + } + + // This function expects the mutex to be already locked. + T ExtractLocked(const Iterator where) { + FIREBASE_ASSERT_MESSAGE(!scheduled_.empty(), + "Trying to pop an entry from an empty queue."); + + T result = std::move(where->value); + scheduled_.erase(where); + cv_.notify_one(); + + return result; + } + + mutable std::mutex mutex_; + std::condition_variable cv_; + Container scheduled_; +}; + +} // namespace async + +namespace internal { + +// A serial queue that executes provided operations on a dedicated background +// thread, using C++11 standard library functionality. +class ExecutorStd : public Executor { + public: + ExecutorStd(); + ~ExecutorStd(); + + void Execute(Operation&& operation) override; + void ExecuteBlocking(Operation&& operation) override; + + DelayedOperation Schedule(Milliseconds delay, + TaggedOperation&& tagged) override; + + bool IsCurrentExecutor() const override; + std::string CurrentExecutorName() const override; + std::string Name() const override; + + bool IsScheduled(Tag tag) const override; + absl::optional<TaggedOperation> PopFromSchedule() override; + + private: + using TimePoint = async::Schedule<Operation>::TimePoint; + // To allow canceling operations, each scheduled operation is assigned + // a monotonically increasing identifier. + using Id = unsigned int; + + // If the operation hasn't yet been run, it will be removed from the queue. + // Otherwise, this function is a no-op. + void TryCancel(Id operation_id); + + Id PushOnSchedule(Operation&& operation, TimePoint when, Tag tag = -1); + + void PollingThread(); + void UnblockQueue(); + Id NextId(); + + // As a convention, assign the epoch time to all operations scheduled for + // immediate execution. Note that it means that an immediate operation is + // always scheduled before any delayed operation, even in the corner case when + // the immediate operation was scheduled after a delayed operation was due + // (but hasn't yet run). + static TimePoint Immediate() { + return TimePoint{}; + } + + struct Entry { + Entry() { + } + Entry(Operation&& operation, + const ExecutorStd::Id id, + const ExecutorStd::Tag tag = kNoTag) + : tagged{tag, std::move(operation)}, id{id} { + } + + bool IsImmediate() const { + return tagged.tag == kNoTag; + } + + static constexpr Tag kNoTag = -1; + TaggedOperation tagged; + Id id = 0; + }; + // Operations scheduled for immediate execution are also put on the schedule + // (with due time set to `Immediate`). + async::Schedule<Entry> schedule_; + + std::thread worker_thread_; + // Used to stop the worker thread. + std::atomic<bool> shutting_down_{false}; + + std::atomic<Id> current_id_{0}; +}; + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_STD_H_ diff --git a/Firestore/core/src/firebase/firestore/util/hashing.h b/Firestore/core/src/firebase/firestore/util/hashing.h new file mode 100644 index 0000000..d8058c8 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/hashing.h @@ -0,0 +1,151 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_HASHING_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_HASHING_H_ + +#include <iterator> +#include <type_traits> + +namespace firebase { +namespace firestore { +namespace util { + +// This is a pretty terrible hash implementation for lack of a better one being +// readily available. It exists as a portability crutch between our existing +// Objective-C code where overriding `-isEqual:` also requires `-hash` and C++ +// where `operator==()` can be defined without defining a hash code. +// +// It's based on the recommendation in Effective Java, Item 9, wherein you +// implement composite hashes like so: +// +// size_t result = first_; +// result = 31 * result + second_; +// result = 31 * result + third_; +// // ... +// return result; +// +// This is the basis of this implementation because that's what the existing +// Objective-C code mostly does by hand. Using this implementation gets the +// same result by calling +// +// return util::Hash(first_, second_, /* ..., */ third_); +// +// TODO(wilhuff): Replace this with whatever Abseil releases. + +namespace impl { + +/** + * Combines a hash_value with whatever accumulated state there is so far. + */ +inline size_t Combine(size_t state, size_t hash_value) { + return 31 * state + hash_value; +} + +/** + * Explicit ordering of hashers, allowing SFINAE without all the enable_if + * cruft. + * + * In order we try: + * * A Hash() member, if defined and the return type is an integral type + * * A std::hash specialization, if available + * * A range-based specialization, valid if either of the above hold on the + * members of the range. + * + * Explicit ordering resolves the ambiguity of the case where a std::hash + * specialization is available, but the type is also a range for whose members + * std::hash is also available, e.g. with std::string. + * + * HashChoice is a recursive type, defined such that HashChoice<0> is the most + * specific type with HashChoice<1> and beyond being progressively less + * specific. This causes the compiler to prioritize the overloads with + * lower-numbered HashChoice types, allowing compilation to succeed even if + * multiple specializations match. + */ +template <int I> +struct HashChoice : HashChoice<I + 1> {}; + +template <> +struct HashChoice<2> {}; + +template <typename K> +size_t InvokeHash(const K& value); + +/** + * Hashes the given value if it defines a Hash() member. + * + * @return The result of `value.Hash()`. + */ +template <typename K> +auto RankedInvokeHash(const K& value, HashChoice<0>) -> decltype(value.Hash()) { + return value.Hash(); +} + +/** + * Hashes the given value if it has a specialization of std::hash. + * + * @return The result of `std::hash<K>{}(value)` + */ +template <typename K> +auto RankedInvokeHash(const K& value, HashChoice<1>) + -> decltype(std::hash<K>{}(value)) { + return std::hash<K>{}(value); +} + +/** + * Hashes the contents of the given range of values if the value_type of the + * range can be hashed. + */ +template <typename Range> +auto RankedInvokeHash(const Range& range, HashChoice<2>) + -> decltype(impl::InvokeHash(*std::begin(range))) { + size_t result = 0; + size_t size = 0; + for (auto&& element : range) { + ++size; + result = Combine(result, InvokeHash(element)); + } + result = Combine(result, size); + return result; +} + +template <typename K> +size_t InvokeHash(const K& value) { + return RankedInvokeHash(value, HashChoice<0>{}); +} + +inline size_t HashInternal(size_t state) { + return state; +} + +template <typename T, typename... Ts> +size_t HashInternal(size_t state, const T& value, const Ts&... rest) { + state = Combine(state, InvokeHash(value)); + return HashInternal(state, rest...); +} + +} // namespace impl + +template <typename... Ts> +size_t Hash(const Ts&... values) { + return impl::HashInternal(0u, values...); +} + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_HASHING_H_ diff --git a/Firestore/core/test/firebase/firestore/immutable/CMakeLists.txt b/Firestore/core/test/firebase/firestore/immutable/CMakeLists.txt index 753e2d0..aa8643b 100644 --- a/Firestore/core/test/firebase/firestore/immutable/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/immutable/CMakeLists.txt @@ -18,6 +18,7 @@ cc_test( array_sorted_map_test.cc testing.h sorted_map_test.cc + sorted_set_test.cc tree_sorted_map_test.cc DEPENDS firebase_firestore_immutable diff --git a/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc b/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc index bcacb50..75353d9 100644 --- a/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc +++ b/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc @@ -18,6 +18,7 @@ #include <numeric> #include <random> +#include <type_traits> #include <unordered_set> #include <utility> @@ -297,6 +298,9 @@ TYPED_TEST(SortedMapTest, MinMax) { } TYPED_TEST(SortedMapTest, IteratorsAreDefaultConstructible) { + ASSERT_TRUE( + std::is_default_constructible<typename TypeParam::const_iterator>::value); + // If this compiles the test has succeeded typename TypeParam::const_iterator iter; (void)iter; diff --git a/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc b/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc new file mode 100644 index 0000000..a4b337c --- /dev/null +++ b/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc @@ -0,0 +1,182 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" + +#include <random> +#include <unordered_set> + +#include "Firestore/core/test/firebase/firestore/immutable/testing.h" + +using firebase::firestore::immutable::impl::SortedMapBase; +using SizeType = SortedMapBase::size_type; + +namespace firebase { +namespace firestore { +namespace immutable { + +template <typename K> +SortedSet<K> ToSet(const std::vector<K>& container) { + SortedSet<K> result; + for (auto&& entry : container) { + result = result.insert(entry); + } + return result; +} + +static const int kLargeNumber = 100; + +TEST(SortedSetTest, EmptyBehavior) { + SortedSet<int> set; + + EXPECT_TRUE(set.empty()); + EXPECT_EQ(0u, set.size()); + + EXPECT_TRUE(NotFound(set, 1)); +} + +TEST(SortedSetTest, Size) { + std::mt19937 rand; + std::uniform_int_distribution<int> dist(0, 999); + + std::unordered_set<int> expected; + + SortedSet<int> set; + for (int i = 0; i < kLargeNumber; ++i) { + int value = dist(rand); + + // The random number sequence can generate duplicates, so the expected size + // won't necessarily depend upon `i`. + expected.insert(value); + + set = set.insert(value); + EXPECT_EQ(expected.size(), set.size()); + } + + for (int i = 0; i < kLargeNumber; ++i) { + int value = dist(rand); + + // The random number sequence can generate duplicates, so the expected size + // won't necessarily depend upon `i`. + expected.erase(value); + + set = set.erase(value); + EXPECT_EQ(expected.size(), set.size()); + } +} + +TEST(SortedSetSet, Find) { + SortedSet<int> set = SortedSet<int>{}.insert(1).insert(2).insert(4); + + EXPECT_TRUE(NotFound(set, 0)); + EXPECT_TRUE(Found(set, 1)); + EXPECT_TRUE(Found(set, 2)); + EXPECT_TRUE(NotFound(set, 3)); + EXPECT_TRUE(Found(set, 4)); + EXPECT_TRUE(NotFound(set, 5)); +} + +TEST(SortedSetTest, IteratorsAreDefaultConstructible) { + static_assert( + std::is_default_constructible<SortedSet<int>::const_iterator>::value, + "is default constructible"); +} + +TEST(SortedSetTest, CanBeConstructedFromSortedMap) { + using Map = SortedMap<int, int>; + + Map map = Map{}.insert(1, 2).insert(3, 4); + auto set = MakeSortedSet(map); + + ASSERT_TRUE(Found(set, 1)); + ASSERT_TRUE(NotFound(set, 2)); + + // Set insertion does not modify the underlying map + set = set.insert(2); + ASSERT_TRUE(Found(set, 2)); + ASSERT_TRUE(NotFound(map, 2)); +} + +TEST(SortedSetTest, Iterator) { + std::vector<int> all = Sequence(kLargeNumber); + SortedSet<int> set = ToSet(Shuffled(all)); + + auto begin = set.begin(); + ASSERT_EQ(0, *begin); + + auto end = set.end(); + ASSERT_EQ(all.size(), static_cast<size_t>(std::distance(begin, end))); + + ASSERT_SEQ_EQ(all, set); +} + +TEST(SortedSetTest, ValuesFrom) { + std::vector<int> all = Sequence(2, 42, 2); + SortedSet<int> set = ToSet(Shuffled(all)); + ASSERT_EQ(20u, set.size()); + + // Test from before keys. + ASSERT_SEQ_EQ(all, set.values_from(0)); + + // Test from after keys. + ASSERT_SEQ_EQ(Empty(), set.values_from(100)); + + // Test from a key in the set: should start at that key. + ASSERT_SEQ_EQ(Sequence(10, 42, 2), set.values_from(10)); + + // Test from in between keys: should start just after that key. + ASSERT_SEQ_EQ(Sequence(12, 42, 2), set.values_from(11)); +} + +TEST(SortedSetTest, ValuesIn) { + std::vector<int> all = Sequence(2, 42, 2); + SortedSet<int> set = ToSet(Shuffled(all)); + ASSERT_EQ(20u, set.size()); + + // Constructs a sequence from `start` up to but not including `end` by 2. + auto Seq = [](int start, int end) { return Sequence(start, end, 2); }; + + ASSERT_SEQ_EQ(Empty(), set.values_in(0, 1)); // before to before + ASSERT_SEQ_EQ(all, set.values_in(0, 100)) // before to after + ASSERT_SEQ_EQ(Seq(2, 6), set.values_in(0, 6)) // before to in set + ASSERT_SEQ_EQ(Seq(2, 8), set.values_in(0, 7)) // before to in between + + ASSERT_SEQ_EQ(Empty(), set.values_in(100, 0)); // after to before + ASSERT_SEQ_EQ(Empty(), set.values_in(100, 110)); // after to after + ASSERT_SEQ_EQ(Empty(), set.values_in(100, 6)); // after to in set + ASSERT_SEQ_EQ(Empty(), set.values_in(100, 7)); // after to in between + + ASSERT_SEQ_EQ(Empty(), set.values_in(6, 0)); // in set to before + ASSERT_SEQ_EQ(Seq(6, 42), set.values_in(6, 100)); // in set to after + ASSERT_SEQ_EQ(Seq(6, 10), set.values_in(6, 10)); // in set to in set + ASSERT_SEQ_EQ(Seq(6, 12), set.values_in(6, 11)); // in set to in between + + ASSERT_SEQ_EQ(Empty(), set.values_in(7, 0)); // in between to before + ASSERT_SEQ_EQ(Seq(8, 42), set.values_in(7, 100)); // in between to after + ASSERT_SEQ_EQ(Seq(8, 10), set.values_in(7, 10)); // in between to key in set + ASSERT_SEQ_EQ(Seq(8, 14), set.values_in(7, 13)); // in between to in between +} + +TEST(SortedSetTest, HashesStdHashable) { + SortedSet<int> set; + + size_t result = util::Hash(set); + (void)result; +} + +} // namespace immutable +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/immutable/testing.h b/Firestore/core/test/firebase/firestore/immutable/testing.h index 9e839c6..8e496dd 100644 --- a/Firestore/core/test/firebase/firestore/immutable/testing.h +++ b/Firestore/core/test/firebase/firestore/immutable/testing.h @@ -18,16 +18,33 @@ #define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_IMMUTABLE_TESTING_H_ #include <algorithm> +#include <string> +#include <type_traits> #include <utility> #include <vector> #include "Firestore/core/src/firebase/firestore/util/secure_random.h" +#include "absl/strings/str_cat.h" #include "gtest/gtest.h" namespace firebase { namespace firestore { namespace immutable { +template <typename K, typename V> +std::string Describe(const std::pair<K, V>& pair) { + return absl::StrCat("(", pair.first, ", ", pair.second, ")"); +} + +// Describes the given item by its std::to_string implementation (if +// std::to_string is defined for V). The return type is not defined directly +// in terms of std::string in order to allow specialization failure to select +// a different overload. +template <typename V> +auto Describe(const V& item) -> decltype(std::to_string(item)) { + return std::to_string(item); +} + template <typename Container, typename K> testing::AssertionResult NotFound(const Container& map, const K& key) { if (map.contains(key)) { @@ -40,11 +57,15 @@ testing::AssertionResult NotFound(const Container& map, const K& key) { return testing::AssertionSuccess(); } else { return testing::AssertionFailure() - << "Should not have found (" << found->first << ", " << found->second - << ")"; + << "Should not have found " << Describe(*found); } } +/** + * Asserts that the given key is found in the given container and that it maps + * to the given value. This only works with map-type containers where value_type + * is `std::pair<K, V>`. + */ template <typename Container, typename K, typename V> testing::AssertionResult Found(const Container& map, const K& key, @@ -67,6 +88,31 @@ testing::AssertionResult Found(const Container& map, } } +/** + * Asserts that the given key is found in the given container without + * necessarily checking that the key maps to any value. This also makes + * this compatible with non-mapped containers where K is the value_type. + */ +template <typename Container, typename K> +testing::AssertionResult Found(const Container& container, const K& key) { + if (!container.contains(key)) { + return testing::AssertionFailure() + << "Did not find key " << key << " using contains()"; + } + + auto found = container.find(key); + if (found == container.end()) { + return testing::AssertionFailure() + << "Did not find key " << key << " using find()"; + } + if (*found == key) { + return testing::AssertionSuccess(); + } else { + return testing::AssertionFailure() + << "Found entry was " << Describe(*found); + } +} + /** Creates an empty vector (for readability). */ inline std::vector<int> Empty() { return {}; diff --git a/Firestore/core/test/firebase/firestore/model/document_key_test.cc b/Firestore/core/test/firebase/firestore/model/document_key_test.cc index 619ee7f..71b78d1 100644 --- a/Firestore/core/test/firebase/firestore/model/document_key_test.cc +++ b/Firestore/core/test/firebase/firestore/model/document_key_test.cc @@ -21,8 +21,11 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "gtest/gtest.h" +using firebase::firestore::testutil::Key; + namespace firebase { namespace firestore { namespace model { @@ -112,16 +115,16 @@ TEST(DocumentKey, IsDocumentKey) { } TEST(DocumentKey, Comparison) { - const DocumentKey abcd({"a", "b", "c", "d"}); - const DocumentKey abcd_too({"a", "b", "c", "d"}); - const DocumentKey xyzw({"x", "y", "z", "w"}); + DocumentKey abcd = Key("a/b/c/d"); + DocumentKey abcd_too = Key("a/b/c/d"); + DocumentKey xyzw = Key("x/y/z/w"); EXPECT_EQ(abcd, abcd_too); EXPECT_NE(abcd, xyzw); - const DocumentKey empty; - const DocumentKey a({"a", "a"}); - const DocumentKey b({"b", "b"}); - const DocumentKey ab({"a", "a", "b", "b"}); + DocumentKey empty; + DocumentKey a = Key("a/a"); + DocumentKey b = Key("b/b"); + DocumentKey ab = Key("a/a/b/b"); EXPECT_FALSE(empty < empty); EXPECT_TRUE(empty <= empty); @@ -148,6 +151,12 @@ TEST(DocumentKey, Comparison) { EXPECT_TRUE(ab >= a); } +TEST(DocumentKey, Comparator) { + DocumentKey abcd = Key("a/b/c/d"); + DocumentKey xyzw = Key("x/y/z/w"); + EXPECT_TRUE(util::Comparator<DocumentKey>{}(abcd, xyzw)); +} + } // namespace model } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc index 96125f7..1dded05 100644 --- a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc @@ -33,18 +33,30 @@ #include <vector> #include "Firestore/Protos/cpp/google/firestore/v1beta1/document.pb.h" +#include "Firestore/core/include/firebase/firestore/firestore_errors.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "google/protobuf/stubs/common.h" #include "google/protobuf/util/message_differencer.h" #include "gtest/gtest.h" +using firebase::firestore::FirestoreErrorCode; +using firebase::firestore::model::DatabaseId; using firebase::firestore::model::FieldValue; using firebase::firestore::model::ObjectValue; using firebase::firestore::remote::Serializer; +using firebase::firestore::testutil::Key; using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; using google::protobuf::util::MessageDifferencer; +#define ASSERT_OK(status) ASSERT_TRUE(StatusOk(status)) +#define ASSERT_NOT_OK(status) ASSERT_FALSE(StatusOk(status)) +#define EXPECT_OK(status) EXPECT_TRUE(StatusOk(status)) +#define EXPECT_NOT_OK(status) EXPECT_FALSE(StatusOk(status)) + TEST(Serializer, CanLinkToNanopb) { // This test doesn't actually do anything interesting as far as actually using // nanopb is concerned but that it can run at all is proof that all the @@ -56,8 +68,10 @@ TEST(Serializer, CanLinkToNanopb) { // Fixture for running serializer tests. class SerializerTest : public ::testing::Test { public: - SerializerTest() : serializer(/*DatabaseId("p", "d")*/) { + SerializerTest() : serializer(kDatabaseId) { } + + const DatabaseId kDatabaseId{"p", "d"}; Serializer serializer; void ExpectRoundTrip(const FieldValue& model, @@ -74,22 +88,69 @@ class SerializerTest : public ::testing::Test { ExpectDeserializationRoundTrip(model, proto, type); } + /** + * Checks the status. Don't use directly; use one of the relevant macros + * instead. eg: + * + * Status good_status = ...; + * ASSERT_OK(good_status); + * + * Status bad_status = ...; + * EXPECT_NOT_OK(bad_status); + */ + testing::AssertionResult StatusOk(const Status& status) { + if (!status.ok()) { + return testing::AssertionFailure() + << "Status should have been ok, but instead contained " + << status.ToString(); + } + return testing::AssertionSuccess(); + } + + template <typename T> + testing::AssertionResult StatusOk(const StatusOr<T>& status) { + return StatusOk(status.status()); + } + + /** + * Ensures that decoding fails with the given status. + * + * @param status the expected (failed) status. Only the code() is verified. + */ + void ExpectFailedStatusDuringDecode(Status status, + const std::vector<uint8_t>& bytes) { + StatusOr<FieldValue> bad_status = serializer.DecodeFieldValue(bytes); + ASSERT_NOT_OK(bad_status); + EXPECT_EQ(status.code(), bad_status.status().code()); + } + google::firestore::v1beta1::Value ValueProto(nullptr_t) { - std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::NullValue(), &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); return proto; } - google::firestore::v1beta1::Value ValueProto(bool b) { + std::vector<uint8_t> EncodeFieldValue(Serializer* serializer, + const FieldValue& fv) { std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::BooleanValue(b), &bytes); - EXPECT_TRUE(status.ok()); + Status status = serializer->EncodeFieldValue(fv, &bytes); + EXPECT_OK(status); + return bytes; + } + + void Mutate(uint8_t* byte, + uint8_t expected_initial_value, + uint8_t new_value) { + ASSERT_EQ(*byte, expected_initial_value); + *byte = new_value; + } + + google::firestore::v1beta1::Value ValueProto(bool b) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::BooleanValue(b)); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -97,10 +158,8 @@ class SerializerTest : public ::testing::Test { } google::firestore::v1beta1::Value ValueProto(int64_t i) { - std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::IntegerValue(i), &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::IntegerValue(i)); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -112,10 +171,8 @@ class SerializerTest : public ::testing::Test { } google::firestore::v1beta1::Value ValueProto(const std::string& s) { - std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::StringValue(s), &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::StringValue(s)); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -128,9 +185,7 @@ class SerializerTest : public ::testing::Test { const google::firestore::v1beta1::Value& proto, FieldValue::Type type) { EXPECT_EQ(type, model.type()); - std::vector<uint8_t> bytes; - Status status = serializer.EncodeFieldValue(model, &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = EncodeFieldValue(&serializer, model); google::firestore::v1beta1::Value actual_proto; bool ok = actual_proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -145,28 +200,28 @@ class SerializerTest : public ::testing::Test { std::vector<uint8_t> bytes(size); bool status = proto.SerializeToArray(bytes.data(), size); EXPECT_TRUE(status); - FieldValue actual_model = serializer.DecodeFieldValue(bytes); + StatusOr<FieldValue> actual_model_status = + serializer.DecodeFieldValue(bytes); + EXPECT_OK(actual_model_status); + FieldValue actual_model = actual_model_status.ValueOrDie(); EXPECT_EQ(type, actual_model.type()); EXPECT_EQ(model, actual_model); } }; -// TODO(rsgowman): whoops! A previous commit performed approx s/Encodes/Writes/, -// but should not have done so here. Change it back in this file. - -TEST_F(SerializerTest, WritesNull) { +TEST_F(SerializerTest, EncodesNull) { FieldValue model = FieldValue::NullValue(); ExpectRoundTrip(model, ValueProto(nullptr), FieldValue::Type::Null); } -TEST_F(SerializerTest, WritesBool) { +TEST_F(SerializerTest, EncodesBool) { for (bool bool_value : {true, false}) { FieldValue model = FieldValue::BooleanValue(bool_value); ExpectRoundTrip(model, ValueProto(bool_value), FieldValue::Type::Boolean); } } -TEST_F(SerializerTest, WritesIntegers) { +TEST_F(SerializerTest, EncodesIntegers) { std::vector<int64_t> cases{0, 1, -1, @@ -181,7 +236,7 @@ TEST_F(SerializerTest, WritesIntegers) { } } -TEST_F(SerializerTest, WritesString) { +TEST_F(SerializerTest, EncodesString) { std::vector<std::string> cases{ "", "a", @@ -204,7 +259,7 @@ TEST_F(SerializerTest, WritesString) { } } -TEST_F(SerializerTest, WritesEmptyMap) { +TEST_F(SerializerTest, EncodesEmptyMap) { FieldValue model = FieldValue::ObjectValueFromMap({}); google::firestore::v1beta1::Value proto; @@ -213,7 +268,7 @@ TEST_F(SerializerTest, WritesEmptyMap) { ExpectRoundTrip(model, proto, FieldValue::Type::Object); } -TEST_F(SerializerTest, WritesNestedObjects) { +TEST_F(SerializerTest, EncodesNestedObjects) { FieldValue model = FieldValue::ObjectValueFromMap({ {"b", FieldValue::TrueValue()}, // TODO(rsgowman): add doubles (once they're supported) @@ -258,7 +313,157 @@ TEST_F(SerializerTest, WritesNestedObjects) { ExpectRoundTrip(model, proto, FieldValue::Type::Object); } +TEST_F(SerializerTest, BadNullValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); + + // Alter the null value from 0 to 1. + Mutate(&bytes[1], /*expected_initial_value=*/0, /*new_value=*/1); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadBoolValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::BooleanValue(true)); + + // Alter the bool value from 1 to 2. (Value values are 0,1) + Mutate(&bytes[1], /*expected_initial_value=*/1, /*new_value=*/2); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadIntegerValue) { + // Encode 'maxint'. This should result in 9 0xff bytes, followed by a 1. + std::vector<uint8_t> bytes = EncodeFieldValue( + &serializer, + FieldValue::IntegerValue(std::numeric_limits<uint64_t>::max())); + ASSERT_EQ(11u, bytes.size()); + for (size_t i = 1; i < bytes.size() - 1; i++) { + ASSERT_EQ(0xff, bytes[i]); + } + + // make the number a bit bigger + Mutate(&bytes[10], /*expected_initial_value=*/1, /*new_value=*/0xff); + bytes.resize(12); + bytes[11] = 0x7f; + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadStringValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::StringValue("a")); + + // Claim that the string length is 5 instead of 1. (The first two bytes are + // used by the encoded tag.) + Mutate(&bytes[2], /*expected_initial_value=*/1, /*new_value=*/5); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadTag) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); + + // The google::firestore::v1beta1::Value value_type oneof currently has tags + // up to 18. For this test, we'll pick a tag that's unlikely to be added in + // the near term but still fits within a uint8_t even when encoded. + // Specifically 31. 0xf8 represents field number 31 encoded as a varint. + Mutate(&bytes[0], /*expected_initial_value=*/0x58, /*new_value=*/0xf8); + + // TODO(rsgowman): The behaviour is *temporarily* slightly different during + // development; this will cause a failed assertion rather than a failed + // status. Remove this EXPECT_ANY_THROW statement (and reenable the + // following commented out statement) once the corresponding assert has been + // removed from serializer.cc. + EXPECT_ANY_THROW(ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes)); + // ExpectFailedStatusDuringDecode( + // Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, TagVarintWiretypeStringMismatch) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::BooleanValue(true)); + + // 0x0a represents a bool value encoded as a string. (We're using a + // boolean_value tag here, but any tag that would be represented by a varint + // would do.) + Mutate(&bytes[0], /*expected_initial_value=*/0x08, /*new_value=*/0x0a); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, TagStringWiretypeVarintMismatch) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::StringValue("foo")); + + // 0x88 represents a string value encoded as a varint. + Mutate(&bytes[0], /*expected_initial_value=*/0x8a, /*new_value=*/0x88); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, IncompleteFieldValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); + ASSERT_EQ(2u, bytes.size()); + + // Remove the (null) payload + ASSERT_EQ(0x00, bytes[1]); + bytes.pop_back(); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, IncompleteTag) { + std::vector<uint8_t> bytes; + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, EncodesKey) { + EXPECT_EQ("projects/p/databases/d/documents", serializer.EncodeKey(Key(""))); + EXPECT_EQ("projects/p/databases/d/documents/one/two/three/four", + serializer.EncodeKey(Key("one/two/three/four"))); +} + +TEST_F(SerializerTest, DecodesKey) { + EXPECT_EQ(Key(""), serializer.DecodeKey("projects/p/databases/d/documents")); + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + "projects/p/databases/d/documents/one/two/three/four")); + // Same, but with a leading slash + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + "/projects/p/databases/d/documents/one/two/three/four")); +} + +TEST_F(SerializerTest, BadKey) { + std::vector<std::string> bad_cases{ + "", // empty (and too short) + "projects/p", // too short + "projects/p/databases/d", // too short + "projects/p/databases/d/documents/odd_number_of_local_elements", + "projects_spelled_wrong/p/databases/d/documents", + "projects/p/databases_spelled_wrong/d/documents", + "projects/not_project_p/databases/d/documents", + "projects/p/databases/not_database_d/documents", + "projects/p/databases/d/not_documents", + }; + + for (const std::string& bad_key : bad_cases) { + EXPECT_ANY_THROW(serializer.DecodeKey(bad_key)); + } +} + // TODO(rsgowman): Test [en|de]coding multiple protos into the same output // vector. - -// TODO(rsgowman): Death test for decoding invalid bytes. diff --git a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt index e5dbec5..2e1e2f9 100644 --- a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt @@ -61,6 +61,60 @@ if(HAVE_OPENSSL_RAND_H) ) endif() +## executors + +cc_test( + firebase_firestore_util_executor_std_test + SOURCES + executor_test.h + executor_test.cc + executor_std_test.cc + async_tests_util.h + DEPENDS + firebase_firestore_util_executor_std +) + +if(HAVE_LIBDISPATCH) + cc_test( + firebase_firestore_util_executor_libdispatch_test + SOURCES + executor_test.h + executor_test.cc + executor_libdispatch_test.cc + async_tests_util.h + DEPENDS + firebase_firestore_util_executor_libdispatch + ) +endif() + +## async queue + +cc_test( + firebase_firestore_util_async_queue_std_test + SOURCES + async_queue_test.h + async_queue_test.cc + async_queue_test_std.cc + async_tests_util.h + DEPENDS + firebase_firestore_util_executor_std + firebase_firestore_util_async_queue +) + +if(HAVE_LIBDISPATCH) + cc_test( + firebase_firestore_util_async_queue_libdispatch_test + SOURCES + async_queue_test.h + async_queue_test.cc + async_queue_test_libdispatch.cc + async_tests_util.h + DEPENDS + firebase_firestore_util_executor_libdispatch + firebase_firestore_util_async_queue + ) +endif() + ## main library cc_test( @@ -69,6 +123,7 @@ cc_test( autoid_test.cc bits_test.cc comparison_test.cc + hashing_test.cc iterator_adaptors_test.cc ordered_code_test.cc status_test.cc diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_test.cc b/Firestore/core/test/firebase/firestore/util/async_queue_test.cc new file mode 100644 index 0000000..bcee2e3 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_queue_test.cc @@ -0,0 +1,184 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/test/firebase/firestore/util/async_queue_test.h" + +#include <chrono> // NOLINT(build/c++11) +#include <future> // NOLINT(build/c++11) +#include <string> + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace { + +// In these generic tests the specific timer ids don't matter. +const TimerId kTimerId1 = TimerId::ListenStreamConnectionBackoff; +const TimerId kTimerId2 = TimerId::ListenStreamIdle; +const TimerId kTimerId3 = TimerId::WriteStreamConnectionBackoff; + +} // namespace + +TEST_P(AsyncQueueTest, Enqueue) { + queue.Enqueue([&] { signal_finished(); }); + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_P(AsyncQueueTest, EnqueueDisallowsNesting) { + queue.Enqueue([&] { // clang-format off + // clang-format on + EXPECT_ANY_THROW(queue.Enqueue([] {})); + signal_finished(); + }); + + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_P(AsyncQueueTest, EnqueueRelaxedWorksFromWithinEnqueue) { + queue.Enqueue([&] { // clang-format off + queue.EnqueueRelaxed([&] { signal_finished(); }); + // clang-format on + }); + + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_P(AsyncQueueTest, EnqueueBlocking) { + bool finished = false; + queue.EnqueueBlocking([&] { finished = true; }); + EXPECT_TRUE(finished); +} + +TEST_P(AsyncQueueTest, EnqueueBlockingDisallowsNesting) { + queue.EnqueueBlocking([&] { // clang-format off + EXPECT_ANY_THROW(queue.EnqueueBlocking([] {});); + // clang-format on + }); +} + +TEST_P(AsyncQueueTest, ExecuteBlockingDisallowsNesting) { + queue.EnqueueBlocking( + [&] { EXPECT_ANY_THROW(queue.ExecuteBlocking([] {});); }); +} + +TEST_P(AsyncQueueTest, VerifyIsCurrentQueueWorksWithOperationInProgress) { + queue.EnqueueBlocking([&] { EXPECT_NO_THROW(queue.VerifyIsCurrentQueue()); }); +} + +TEST_P(AsyncQueueTest, CanScheduleOperationsInTheFuture) { + std::string steps; + + queue.Enqueue([&steps] { steps += '1'; }); + queue.Enqueue([&] { + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(5), kTimerId1, [&] { + steps += '4'; + signal_finished(); + }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(1), kTimerId2, + [&steps] { steps += '3'; }); + queue.EnqueueRelaxed([&steps] { steps += '2'; }); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_EQ(steps, "1234"); +} + +TEST_P(AsyncQueueTest, CanCancelDelayedOperations) { + std::string steps; + + queue.Enqueue([&] { + // Queue everything from the queue to ensure nothing completes before we + // cancel. + + queue.EnqueueRelaxed([&steps] { steps += '1'; }); + + DelayedOperation delayed_operation = queue.EnqueueAfterDelay( + AsyncQueue::Milliseconds(1), kTimerId1, [&steps] { steps += '2'; }); + + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(5), kTimerId2, [&] { + steps += '3'; + signal_finished(); + }); + + EXPECT_TRUE(queue.IsScheduled(kTimerId1)); + delayed_operation.Cancel(); + EXPECT_FALSE(queue.IsScheduled(kTimerId1)); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_EQ(steps, "13"); + EXPECT_FALSE(queue.IsScheduled(kTimerId1)); +} + +TEST_P(AsyncQueueTest, CanCallCancelOnDelayedOperationAfterTheOperationHasRun) { + DelayedOperation delayed_operation; + queue.Enqueue([&] { + delayed_operation = queue.EnqueueAfterDelay( + AsyncQueue::Milliseconds(10), kTimerId1, [&] { signal_finished(); }); + EXPECT_TRUE(queue.IsScheduled(kTimerId1)); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_FALSE(queue.IsScheduled(kTimerId1)); + EXPECT_NO_THROW(delayed_operation.Cancel()); +} + +TEST_P(AsyncQueueTest, CanManuallyDrainAllDelayedOperationsForTesting) { + std::string steps; + + queue.Enqueue([&] { + queue.EnqueueRelaxed([&steps] { steps += '1'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(20000), kTimerId1, + [&] { steps += '4'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(10000), kTimerId2, + [&steps] { steps += '3'; }); + queue.EnqueueRelaxed([&steps] { steps += '2'; }); + signal_finished(); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + queue.RunScheduledOperationsUntil(TimerId::All); + EXPECT_EQ(steps, "1234"); +} + +TEST_P(AsyncQueueTest, CanManuallyDrainSpecificDelayedOperationsForTesting) { + std::string steps; + + queue.Enqueue([&] { + queue.EnqueueRelaxed([&] { steps += '1'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(20000), kTimerId1, + [&steps] { steps += '5'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(10000), kTimerId2, + [&steps] { steps += '3'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(15000), kTimerId3, + [&steps] { steps += '4'; }); + queue.EnqueueRelaxed([&] { steps += '2'; }); + signal_finished(); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + queue.RunScheduledOperationsUntil(kTimerId3); + EXPECT_EQ(steps, "1234"); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_test.h b/Firestore/core/test/firebase/firestore/util/async_queue_test.h new file mode 100644 index 0000000..61c7ab6 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_queue_test.h @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_TEST_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_TEST_H_ + +#include <memory> + +#include "gtest/gtest.h" + +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/test/firebase/firestore/util/async_tests_util.h" + +namespace firebase { +namespace firestore { +namespace util { + +using FactoryFunc = std::unique_ptr<internal::Executor> (*)(); + +class AsyncQueueTest : public TestWithTimeoutMixin, + public ::testing::TestWithParam<FactoryFunc> { + public: + // `GetParam()` must return a factory function. + AsyncQueueTest() : queue{GetParam()()} { + } + + AsyncQueue queue; +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_TEST_H_ diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_test_libdispatch.cc b/Firestore/core/test/firebase/firestore/util/async_queue_test_libdispatch.cc new file mode 100644 index 0000000..b4b9c63 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_queue_test_libdispatch.cc @@ -0,0 +1,86 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/test/firebase/firestore/util/async_queue_test.h" + +#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" + +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace { + +dispatch_queue_t CreateDispatchQueue() { + return dispatch_queue_create("AsyncQueueTests", DISPATCH_QUEUE_SERIAL); +} + +std::unique_ptr<internal::Executor> CreateExecutorFromQueue( + const dispatch_queue_t queue) { + return absl::make_unique<internal::ExecutorLibdispatch>(queue); +} + +std::unique_ptr<internal::Executor> CreateExecutorLibdispatch() { + return CreateExecutorFromQueue(CreateDispatchQueue()); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(AsyncQueueLibdispatch, + AsyncQueueTest, + ::testing::Values(CreateExecutorLibdispatch)); + +class AsyncQueueTestLibdispatchOnly : public TestWithTimeoutMixin, + public ::testing::Test { + public: + AsyncQueueTestLibdispatchOnly() + : underlying_queue{CreateDispatchQueue()}, + queue{CreateExecutorFromQueue(underlying_queue)} { + } + + dispatch_queue_t underlying_queue; + AsyncQueue queue; +}; + +// Additional tests to see how libdispatch-based version of `AsyncQueue` +// interacts with raw usage of libdispatch. + +TEST_F(AsyncQueueTestLibdispatchOnly, SameQueueIsAllowedForUnownedActions) { + internal::DispatchAsync(underlying_queue, [this] { + queue.Enqueue([this] { signal_finished(); }); + }); + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_F(AsyncQueueTestLibdispatchOnly, + VerifyIsCurrentQueueRequiresOperationInProgress) { + internal::DispatchSync(underlying_queue, [this] { + EXPECT_ANY_THROW(queue.VerifyIsCurrentQueue()); + }); +} + +TEST_F(AsyncQueueTestLibdispatchOnly, + VerifyIsCurrentQueueRequiresBeingCalledAsync) { + ASSERT_NE(underlying_queue, dispatch_get_main_queue()); + EXPECT_ANY_THROW(queue.VerifyIsCurrentQueue()); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_test_std.cc b/Firestore/core/test/firebase/firestore/util/async_queue_test_std.cc new file mode 100644 index 0000000..9e69ad0 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_queue_test_std.cc @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/test/firebase/firestore/util/async_queue_test.h" + +#include "Firestore/core/src/firebase/firestore/util/executor_std.h" + +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace { + +std::unique_ptr<internal::Executor> ExecutorFactory() { + return absl::make_unique<internal::ExecutorStd>(); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(AsyncQueueStd, + AsyncQueueTest, + ::testing::Values(ExecutorFactory)); +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/async_tests_util.h b/Firestore/core/test/firebase/firestore/util/async_tests_util.h new file mode 100644 index 0000000..422745b --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_tests_util.h @@ -0,0 +1,90 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_TESTS_UTIL_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_TESTS_UTIL_H_ + +#include <chrono> // NOLINT(build/c++11) +#include <cstdlib> +#include <future> // NOLINT(build/c++11) + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +inline std::chrono::time_point<std::chrono::system_clock, + std::chrono::milliseconds> +now() { + return std::chrono::time_point_cast<std::chrono::milliseconds>( + std::chrono::system_clock::now()); +} + +constexpr auto kTimeout = std::chrono::seconds(5); + +// Waits for the future to become ready and returns whether it timed out. +inline bool Await(const std::future<void>& future, + const std::chrono::milliseconds timeout = kTimeout) { + return future.wait_for(timeout) == std::future_status::ready; +} + +// Unfortunately, the future returned from std::async blocks in its destructor +// until the async call is finished. If the function called from std::async is +// buggy and hangs forever, the future's destructor will also hang forever. To +// avoid all tests freezing, the only thing to do is to abort (which skips +// destructors). +inline void Abort() { + ADD_FAILURE(); + std::abort(); +} + +// Calls std::abort if the future times out. +inline void AbortOnTimeout(const std::future<void>& future) { + if (!Await(future, kTimeout)) { + Abort(); + } +} + +// The macro calls AbortOnTimeout, but preserves stack trace. +#define ABORT_ON_TIMEOUT(future) \ + do { \ + SCOPED_TRACE("Async operation timed out, aborting..."); \ + AbortOnTimeout(future); \ + } while (0) + +class TestWithTimeoutMixin { + public: + TestWithTimeoutMixin() : signal_finished{[] {}} { + } + + // Googletest doesn't contain built-in functionality to block until an async + // operation completes, and there is no timeout by default. Work around both + // by resolving a packaged_task in the async operation and blocking on the + // associated future (with timeout). + bool WaitForTestToFinish(const std::chrono::seconds timeout = kTimeout) { + return signal_finished.get_future().wait_for(timeout) == + std::future_status::ready; + } + + std::packaged_task<void()> signal_finished; +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_TESTS_UTIL_H_ diff --git a/Firestore/core/test/firebase/firestore/util/executor_libdispatch_test.cc b/Firestore/core/test/firebase/firestore/util/executor_libdispatch_test.cc new file mode 100644 index 0000000..0167c83 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/executor_libdispatch_test.cc @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/test/firebase/firestore/util/executor_test.h" + +#include <memory> + +#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace { + +std::unique_ptr<internal::Executor> ExecutorFactory() { + return absl::make_unique<internal::ExecutorLibdispatch>(); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(ExecutorTestLibdispatch, + ExecutorTest, + ::testing::Values(ExecutorFactory)); + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/executor_std_test.cc b/Firestore/core/test/firebase/firestore/util/executor_std_test.cc new file mode 100644 index 0000000..43cad60 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/executor_std_test.cc @@ -0,0 +1,240 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/test/firebase/firestore/util/executor_test.h" + +#include <chrono> // NOLINT(build/c++11) +#include <cstdlib> +#include <future> // NOLINT(build/c++11) +#include <string> +#include <thread> // NOLINT(build/c++11) + +#include "Firestore/core/src/firebase/firestore/util/executor_std.h" +#include "Firestore/core/test/firebase/firestore/util/async_tests_util.h" +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace chr = std::chrono; +using async::Schedule; + +class ScheduleTest : public ::testing::Test { + public: + ScheduleTest() : start_time{now()} { + } + + using ScheduleT = Schedule<int>; + + ScheduleT schedule; + ScheduleT::TimePoint start_time; +}; + +// Schedule tests + +TEST_F(ScheduleTest, PopIfDue_Immediate) { + EXPECT_FALSE(schedule.PopIfDue().has_value()); + + // Push values in a deliberately non-sorted order. + schedule.Push(3, start_time); + schedule.Push(1, start_time); + schedule.Push(2, start_time); + EXPECT_FALSE(schedule.empty()); + EXPECT_EQ(schedule.size(), 3u); + + EXPECT_EQ(schedule.PopIfDue().value(), 3); + EXPECT_EQ(schedule.PopIfDue().value(), 1); + EXPECT_EQ(schedule.PopIfDue().value(), 2); + EXPECT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_TRUE(schedule.empty()); + EXPECT_EQ(schedule.size(), 0u); +} + +TEST_F(ScheduleTest, PopIfDue_Delayed) { + schedule.Push(1, start_time + chr::milliseconds(5)); + schedule.Push(2, start_time + chr::milliseconds(3)); + schedule.Push(3, start_time + chr::milliseconds(1)); + + EXPECT_FALSE(schedule.PopIfDue().has_value()); + std::this_thread::sleep_for(chr::milliseconds(5)); + + EXPECT_EQ(schedule.PopIfDue().value(), 3); + EXPECT_EQ(schedule.PopIfDue().value(), 2); + EXPECT_EQ(schedule.PopIfDue().value(), 1); + EXPECT_TRUE(schedule.empty()); +} + +TEST_F(ScheduleTest, PopBlocking) { + schedule.Push(1, start_time + chr::milliseconds(3)); + EXPECT_FALSE(schedule.PopIfDue().has_value()); + + EXPECT_EQ(schedule.PopBlocking(), 1); + EXPECT_GE(now(), start_time + chr::milliseconds(3)); + EXPECT_TRUE(schedule.empty()); +} + +TEST_F(ScheduleTest, RemoveIf) { + schedule.Push(1, start_time); + schedule.Push(2, now() + chr::minutes(1)); + + auto maybe_removed = schedule.RemoveIf([](const int v) { return v == 1; }); + EXPECT_TRUE(maybe_removed.has_value()); + EXPECT_EQ(maybe_removed.value(), 1); + + // Non-existent value. + maybe_removed = schedule.RemoveIf([](const int v) { return v == 1; }); + EXPECT_FALSE(maybe_removed.has_value()); + + maybe_removed = schedule.RemoveIf([](const int v) { return v == 2; }); + EXPECT_TRUE(maybe_removed.has_value()); + EXPECT_EQ(maybe_removed.value(), 2); + EXPECT_TRUE(schedule.empty()); +} + +TEST_F(ScheduleTest, Ordering) { + schedule.Push(11, start_time + chr::milliseconds(5)); + schedule.Push(1, start_time); + schedule.Push(2, start_time); + schedule.Push(9, start_time + chr::milliseconds(2)); + schedule.Push(3, start_time); + schedule.Push(10, start_time + chr::milliseconds(3)); + schedule.Push(12, start_time + chr::milliseconds(5)); + schedule.Push(4, start_time); + schedule.Push(5, start_time); + schedule.Push(6, start_time); + schedule.Push(8, start_time + chr::milliseconds(1)); + schedule.Push(7, start_time); + + std::vector<int> values; + while (!schedule.empty()) { + values.push_back(schedule.PopBlocking()); + } + const std::vector<int> expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + EXPECT_EQ(values, expected); +} + +TEST_F(ScheduleTest, AddingEntryUnblocksEmptyQueue) { + const auto future = std::async(std::launch::async, [&] { + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 1); + }); + + std::this_thread::sleep_for(chr::milliseconds(5)); + schedule.Push(1, start_time); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingUnblocksOnNewPastDueEntries) { + const auto far_away = start_time + chr::seconds(10); + schedule.Push(5, far_away); + + const auto future = std::async(std::launch::async, [&] { + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 3); + }); + + std::this_thread::sleep_for(chr::milliseconds(5)); + schedule.Push(3, start_time); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingAdjustsWaitTimeOnNewSoonerEntries) { + const auto far_away = start_time + chr::seconds(10); + schedule.Push(5, far_away); + + const auto future = std::async(std::launch::async, [&] { + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 3); + // Make sure schedule hasn't been waiting longer than necessary. + EXPECT_LT(now(), far_away); + }); + + std::this_thread::sleep_for(chr::milliseconds(5)); + schedule.Push(3, start_time + chr::milliseconds(100)); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingCanReadjustTimeIfSeveralElementsAreAdded) { + const auto far_away = start_time + chr::seconds(5); + const auto very_far_away = start_time + chr::seconds(10); + schedule.Push(3, very_far_away); + + const auto future = std::async(std::launch::async, [&] { + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 1); + EXPECT_LT(now(), far_away); + }); + + std::this_thread::sleep_for(chr::milliseconds(5)); + schedule.Push(2, far_away); + std::this_thread::sleep_for(chr::milliseconds(1)); + schedule.Push(1, start_time + chr::milliseconds(100)); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingNoticesRemovals) { + const auto future = std::async(std::launch::async, [&] { + schedule.Push(1, start_time + chr::milliseconds(50)); + schedule.Push(2, start_time + chr::milliseconds(100)); + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 2); + }); + + while (schedule.empty()) { + std::this_thread::sleep_for(chr::milliseconds(1)); + } + const auto maybe_removed = + schedule.RemoveIf([](const int v) { return v == 1; }); + EXPECT_EQ(maybe_removed.value(), 1); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingIsNotAffectedByIrrelevantRemovals) { + const auto future = std::async(std::launch::async, [&] { + schedule.Push(1, start_time + chr::milliseconds(50)); + schedule.Push(2, start_time + chr::seconds(10)); + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 1); + }); + + while (schedule.empty()) { + std::this_thread::sleep_for(chr::milliseconds(1)); + } + const auto maybe_removed = + schedule.RemoveIf([](const int v) { return v == 2; }); + EXPECT_EQ(maybe_removed.value(), 2); + ABORT_ON_TIMEOUT(future); +} + +// ExecutorStd tests + +namespace { + +inline std::unique_ptr<internal::Executor> ExecutorFactory() { + return absl::make_unique<internal::ExecutorStd>(); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(ExecutorTestStd, + ExecutorTest, + ::testing::Values(ExecutorFactory)); + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/executor_test.cc b/Firestore/core/test/firebase/firestore/util/executor_test.cc new file mode 100644 index 0000000..5cf389b --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/executor_test.cc @@ -0,0 +1,110 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/test/firebase/firestore/util/executor_test.h" + +#include <chrono> // NOLINT(build/c++11) +#include <cstdlib> +#include <future> // NOLINT(build/c++11) +#include <string> +#include <thread> // NOLINT(build/c++11) + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace chr = std::chrono; +using internal::Executor; + +namespace { + +DelayedOperation Schedule(Executor* const executor, + const Executor::Milliseconds delay, + Executor::Operation&& operation) { + const Executor::Tag no_tag = -1; + return executor->Schedule( + delay, Executor::TaggedOperation{no_tag, std::move(operation)}); +} + +} // namespace + +TEST_P(ExecutorTest, Execute) { + executor->Execute([&] { signal_finished(); }); + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_P(ExecutorTest, DestructorDoesNotBlockIfThereArePendingTasks) { + const auto future = std::async(std::launch::async, [&] { + auto another_executor = GetParam()(); + Schedule(another_executor.get(), chr::minutes(5), [] {}); + Schedule(another_executor.get(), chr::minutes(10), [] {}); + // Destructor shouldn't block waiting for the 5/10-minute-away operations. + }); + + ABORT_ON_TIMEOUT(future); +} + +TEST_P(ExecutorTest, CanScheduleOperationsInTheFuture) { + std::string steps; + + executor->Execute([&steps] { steps += '1'; }); + Schedule(executor.get(), Executor::Milliseconds(5), [&] { + steps += '4'; + signal_finished(); + }); + Schedule(executor.get(), Executor::Milliseconds(1), + [&steps] { steps += '3'; }); + executor->Execute([&steps] { steps += '2'; }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_EQ(steps, "1234"); +} + +TEST_P(ExecutorTest, CanCancelDelayedOperations) { + std::string steps; + + executor->Execute([&] { + executor->Execute([&steps] { steps += '1'; }); + + DelayedOperation delayed_operation = Schedule( + executor.get(), Executor::Milliseconds(1), [&steps] { steps += '2'; }); + + Schedule(executor.get(), Executor::Milliseconds(5), [&] { + steps += '3'; + signal_finished(); + }); + + delayed_operation.Cancel(); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_EQ(steps, "13"); +} + +TEST_P(ExecutorTest, DelayedOperationIsValidAfterTheOperationHasRun) { + DelayedOperation delayed_operation = Schedule( + executor.get(), Executor::Milliseconds(1), [&] { signal_finished(); }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_NO_THROW(delayed_operation.Cancel()); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/executor_test.h b/Firestore/core/test/firebase/firestore/util/executor_test.h new file mode 100644 index 0000000..8b78d50 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/executor_test.h @@ -0,0 +1,46 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_EXECUTOR_TEST_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_EXECUTOR_TEST_H_ + +#include <memory> + +#include "gtest/gtest.h" + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/test/firebase/firestore/util/async_tests_util.h" + +namespace firebase { +namespace firestore { +namespace util { + +using FactoryFunc = std::unique_ptr<internal::Executor> (*)(); + +class ExecutorTest : public TestWithTimeoutMixin, + public ::testing::TestWithParam<FactoryFunc> { + public: + // `GetParam()` must return a factory function. + ExecutorTest() : executor{GetParam()()} { + } + + std::unique_ptr<internal::Executor> executor; +}; +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_EXECUTOR_TEST_H_ diff --git a/Firestore/core/test/firebase/firestore/util/hashing_test.cc b/Firestore/core/test/firebase/firestore/util/hashing_test.cc new file mode 100644 index 0000000..e5d9ff8 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/hashing_test.cc @@ -0,0 +1,105 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Firestore/core/src/firebase/firestore/util/hashing.h" + +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +struct HasHashMember { + size_t Hash() const { + return 42; + } +}; + +TEST(HashingTest, Int) { + ASSERT_EQ(std::hash<int>{}(0), Hash(0)); +} + +TEST(HashingTest, Float) { + ASSERT_EQ(std::hash<double>{}(1.0), Hash(1.0)); +} + +TEST(HashingTest, String) { + ASSERT_EQ(std::hash<std::string>{}("foobar"), Hash(std::string{"foobar"})); +} + +TEST(HashingTest, StringView) { + // For StringView we expect the range-based hasher to kick in. This is + // basically terrible, but no worse than Java's `String.hashCode()`. Another + // possibility would be just to create a temporary std::string and std::hash + // that, but that requires an explicit specialization. Since we're only + // defining this for compatibility with Objective-C and not really sensitive + // to performance or hash quality here, this is good enough. + size_t expected = 'a'; + expected = 31u * expected + 1; + ASSERT_EQ(expected, Hash(absl::string_view{"a"})); +} + +TEST(HashingTest, SizeT) { + ASSERT_EQ(42u, Hash(size_t{42u})); +} + +TEST(HashingTest, Array) { + int values[] = {0, 1, 2}; + + size_t expected = 0; + expected = 31 * expected + 1; + expected = 31 * expected + 2; + expected = 31 * expected + 3; // length of array + ASSERT_EQ(expected, Hash(values)); +} + +TEST(HashingTest, HasHashMember) { + ASSERT_EQ(static_cast<size_t>(42), Hash(HasHashMember{})); +} + +TEST(HashingTest, RangeOfStdHashable) { + std::vector<int> values{42}; + ASSERT_EQ(31u * 42u + 1, Hash(values)); + + std::vector<int> values_leading_zero{0, 42}; + std::vector<int> values_trailing_zero{42, 0}; + + EXPECT_NE(Hash(values), Hash(values_leading_zero)); + EXPECT_NE(Hash(values), Hash(values_trailing_zero)); + EXPECT_NE(Hash(values_leading_zero), Hash(values_trailing_zero)); +} + +TEST(HashingTest, RangeOfHashMember) { + std::vector<HasHashMember> values{HasHashMember{}}; + ASSERT_EQ(31u * 42u + 1, Hash(values)); +} + +TEST(HashingTest, Composite) { + // Verify the result ends up as if hand-rolled + EXPECT_EQ(1u, Hash(1)); + EXPECT_EQ(31u, Hash(1, 0)); + EXPECT_EQ(31u * 31u, Hash(1, 0, 0)); + + size_t expected = Hash(1); + expected = 31 * expected + Hash(2); + expected = 31 * expected + Hash(3); + EXPECT_EQ(expected, Hash(1, 2, 3)); +} + +} // namespace util +} // namespace firestore +} // namespace firebase |