diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 9f5359310c2..77d823a54df 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -24,6 +24,10 @@ `FIRServerTimestampBehavior` on `DocumentSnapshot`. Instead of calling `data(SnapshotOptions.serverTimestampBehavior(.estimate))` call `data(serverTimestampBehavior: .estimate)`. Changed `get` similarly. +- [changed] Added ability to control whether DocumentReference.getDocument() and + 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.) # 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 d20dc48c2ab..dd643511d53 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; + 6161B5032047140C00A99DBB /* FIRFirestoreSourceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */; }; 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */; }; 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; 7346E61D20325C6900FD6CEF /* FSTDispatchQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7346E61C20325C6900FD6CEF /* FSTDispatchQueueTests.mm */; }; @@ -356,6 +357,7 @@ 6003F5AF195388D20070C39A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 6003F5B7195388D20070C39A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = ""; }; 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFirestoreSourceTests.mm; sourceTree = ""; }; 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 = ""; }; 7346E61C20325C6900FD6CEF /* FSTDispatchQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDispatchQueueTests.mm; sourceTree = ""; }; @@ -885,6 +887,7 @@ isa = PBXGroup; children = ( 73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */, + 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */, 5492E070202154D600B64F25 /* FIRCursorTests.mm */, 5492E06C202154D500B64F25 /* FIRDatabaseTests.mm */, 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */, @@ -1559,6 +1562,7 @@ buildActionMask = 2147483647; files = ( 73866AA12082B0A5009BB4FF /* FIRArrayTransformTests.mm in Sources */, + 6161B5032047140C00A99DBB /* FIRFirestoreSourceTests.mm in Sources */, 5492E076202154D600B64F25 /* FIRValidationTests.mm in Sources */, 5492E072202154D600B64F25 /* FIRQueryTests.mm in Sources */, 5491BC731FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */, diff --git a/Firestore/Example/SwiftBuildTest/main.swift b/Firestore/Example/SwiftBuildTest/main.swift index 00839c48832..f61da2bb5c6 100644 --- a/Firestore/Example/SwiftBuildTest/main.swift +++ b/Firestore/Example/SwiftBuildTest/main.swift @@ -32,8 +32,10 @@ func main() { addDocument(to: collectionRef) readDocument(at: documentRef) + readDocumentWithSource(at: documentRef) readDocuments(matching: query) + readDocumentsWithSource(matching: query) listenToDocument(at: documentRef) @@ -230,6 +232,15 @@ func readDocument(at docRef: DocumentReference) { } } +func readDocumentWithSource(at docRef: DocumentReference) { + docRef.getDocument(source: FirestoreSource.default) { document, error in + } + docRef.getDocument(source: .server) { document, error in + } + docRef.getDocument(source: FirestoreSource.cache) { document, error in + } +} + func readDocuments(matching query: Query) { query.getDocuments { querySnapshot, error in // TODO(mikelehen): Figure out how to make "for..in" syntax work @@ -240,6 +251,15 @@ func readDocuments(matching query: Query) { } } +func readDocumentsWithSource(matching query: Query) { + query.getDocuments(source: FirestoreSource.default) { querySnapshot, error in + } + query.getDocuments(source: .server) { querySnapshot, error in + } + query.getDocuments(source: FirestoreSource.cache) { querySnapshot, error in + } +} + func listenToDocument(at docRef: DocumentReference) { let listener = docRef.addSnapshotListener { document, error in if let error = error { diff --git a/Firestore/Example/Tests/Integration/API/FIRFirestoreSourceTests.mm b/Firestore/Example/Tests/Integration/API/FIRFirestoreSourceTests.mm new file mode 100644 index 00000000000..0f844c6dd14 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRFirestoreSourceTests.mm @@ -0,0 +1,648 @@ +/* + * 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. + */ + +#import + +#import + +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" + +@interface FIRFirestoreSourceTests : FSTIntegrationTestCase +@end + +@implementation FIRFirestoreSourceTests + +- (void)testGetDocumentWhileOnlineWithDefaultSource { + FIRDocumentReference *doc = [self documentRef]; + + // set document to a known value + NSDictionary *initialData = @{@"key" : @"value"}; + [self writeDocumentRef:doc data:initialData]; + + // get doc and ensure that it exists, is *not* from the cache, and matches + // the initialData. + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertTrue(result.exists); + XCTAssertFalse(result.metadata.fromCache); + XCTAssertFalse(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(result.data, initialData); +} + +- (void)testGetCollectionWhileOnlineWithDefaultSource { + FIRCollectionReference *col = [self collectionRef]; + + // set a few documents to known values + NSDictionary *> *initialDocs = @{ + @"doc1" : @{@"key1" : @"value1"}, + @"doc2" : @{@"key2" : @"value2"}, + @"doc3" : @{@"key3" : @"value3"} + }; + [self writeAllDocuments:initialDocs toCollection:col]; + + // get docs and ensure they are *not* from the cache, and match the + // initialDocs. + FIRQuerySnapshot *result = [self readDocumentSetForRef:col]; + XCTAssertFalse(result.metadata.fromCache); + XCTAssertFalse(result.metadata.hasPendingWrites); + XCTAssertEqualObjects( + FIRQuerySnapshotGetData(result), + (@[ @{@"key1" : @"value1"}, @{@"key2" : @"value2"}, @{@"key3" : @"value3"} ])); + XCTAssertEqualObjects(FIRQuerySnapshotGetDocChangesData(result), (@[ + @[ @(FIRDocumentChangeTypeAdded), @"doc1", @{@"key1" : @"value1"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc2", @{@"key2" : @"value2"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc3", @{@"key3" : @"value3"} ] + ])); +} + +- (void)testGetDocumentWhileOfflineWithDefaultSource { + FIRDocumentReference *doc = [self documentRef]; + + // set document to a known value + NSDictionary *initialData = @{@"key1" : @"value1"}; + [self writeDocumentRef:doc data:initialData]; + + // go offline for the rest of this test + [self disableNetwork]; + + // update the doc (though don't wait for a server response. We're offline; so + // that ain't happening!). This allows us to further distinguished cached vs + // server responses below. + NSDictionary *newData = @{@"key2" : @"value2"}; + [doc setData:newData + completion:^(NSError *_Nullable error) { + XCTAssertTrue(false, "Because we're offline, this should never occur."); + }]; + + // get doc and ensure it exists, *is* from the cache, and matches the + // newData. + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertTrue(result.exists); + XCTAssertTrue(result.metadata.fromCache); + XCTAssertTrue(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(result.data, newData); +} + +- (void)testGetCollectionWhileOfflineWithDefaultSource { + FIRCollectionReference *col = [self collectionRef]; + + // set a few documents to known values + NSDictionary *> *initialDocs = @{ + @"doc1" : @{@"key1" : @"value1"}, + @"doc2" : @{@"key2" : @"value2"}, + @"doc3" : @{@"key3" : @"value3"} + }; + [self writeAllDocuments:initialDocs toCollection:col]; + + // go offline for the rest of this test + [self disableNetwork]; + + // update the docs (though don't wait for a server response. We're offline; so + // that ain't happening!). This allows us to further distinguished cached vs + // server responses below. + [[col documentWithPath:@"doc2"] setData:@{@"key2b" : @"value2b"} options:FIRSetOptions.merge]; + [[col documentWithPath:@"doc3"] setData:@{@"key3b" : @"value3b"}]; + [[col documentWithPath:@"doc4"] setData:@{@"key4" : @"value4"}]; + + // get docs and ensure they *are* from the cache, and matches the updated data. + FIRQuerySnapshot *result = [self readDocumentSetForRef:col]; + XCTAssertTrue(result.metadata.fromCache); + XCTAssertTrue(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), (@[ + @{@"key1" : @"value1"}, @{@"key2" : @"value2", @"key2b" : @"value2b"}, + @{@"key3b" : @"value3b"}, @{@"key4" : @"value4"} + ])); + XCTAssertEqualObjects( + FIRQuerySnapshotGetDocChangesData(result), (@[ + @[ @(FIRDocumentChangeTypeAdded), @"doc1", @{@"key1" : @"value1"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc2", @{@"key2" : @"value2", @"key2b" : @"value2b"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc3", @{@"key3b" : @"value3b"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc4", @{@"key4" : @"value4"} ] + ])); +} + +- (void)testGetDocumentWhileOnlineCacheOnly { + FIRDocumentReference *doc = [self documentRef]; + + // set document to a known value + NSDictionary *initialData = @{@"key" : @"value"}; + [self writeDocumentRef:doc data:initialData]; + + // get doc and ensure that it exists, *is* from the cache, and matches + // the initialData. + FIRDocumentSnapshot *result = [self readDocumentForRef:doc source:FIRFirestoreSourceCache]; + XCTAssertTrue(result.exists); + XCTAssertTrue(result.metadata.fromCache); + XCTAssertFalse(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(result.data, initialData); +} + +- (void)testGetCollectionWhileOnlineCacheOnly { + FIRCollectionReference *col = [self collectionRef]; + + // set a few documents to a known value + NSDictionary *> *initialDocs = @{ + @"doc1" : @{@"key1" : @"value1"}, + @"doc2" : @{@"key2" : @"value2"}, + @"doc3" : @{@"key3" : @"value3"}, + }; + [self writeAllDocuments:initialDocs toCollection:col]; + + // get docs and ensure they *are* from the cache, and matches the + // initialDocs. + FIRQuerySnapshot *result = [self readDocumentSetForRef:col source:FIRFirestoreSourceCache]; + XCTAssertTrue(result.metadata.fromCache); + XCTAssertFalse(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), (@[ + @{@"key1" : @"value1"}, + @{@"key2" : @"value2"}, + @{@"key3" : @"value3"}, + ])); + XCTAssertEqualObjects(FIRQuerySnapshotGetDocChangesData(result), (@[ + @[ @(FIRDocumentChangeTypeAdded), @"doc1", @{@"key1" : @"value1"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc2", @{@"key2" : @"value2"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc3", @{@"key3" : @"value3"} ] + ])); +} + +- (void)testGetDocumentWhileOfflineCacheOnly { + FIRDocumentReference *doc = [self documentRef]; + + // set document to a known value + NSDictionary *initialData = @{@"key1" : @"value1"}; + [self writeDocumentRef:doc data:initialData]; + + // go offline for the rest of this test + [self disableNetwork]; + + // update the doc (though don't wait for a server response. We're offline; so + // that ain't happening!). This allows us to further distinguished cached vs + // server responses below. + NSDictionary *newData = @{@"key2" : @"value2"}; + [doc setData:newData + completion:^(NSError *_Nullable error) { + XCTFail("Because we're offline, this should never occur."); + }]; + + // get doc and ensure it exists, *is* from the cache, and matches the + // newData. + FIRDocumentSnapshot *result = [self readDocumentForRef:doc source:FIRFirestoreSourceCache]; + XCTAssertTrue(result.exists); + XCTAssertTrue(result.metadata.fromCache); + XCTAssertTrue(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(result.data, newData); +} + +- (void)testGetCollectionWhileOfflineCacheOnly { + FIRCollectionReference *col = [self collectionRef]; + + // set a few documents to a known value + NSDictionary *> *initialDocs = @{ + @"doc1" : @{@"key1" : @"value1"}, + @"doc2" : @{@"key2" : @"value2"}, + @"doc3" : @{@"key3" : @"value3"}, + }; + [self writeAllDocuments:initialDocs toCollection:col]; + + // go offline for the rest of this test + [self disableNetwork]; + + // update the docs (though don't wait for a server response. We're offline; so + // that ain't happening!). This allows us to further distinguished cached vs + // server responses below. + [[col documentWithPath:@"doc2"] setData:@{@"key2b" : @"value2b"} options:FIRSetOptions.merge]; + [[col documentWithPath:@"doc3"] setData:@{@"key3b" : @"value3b"}]; + [[col documentWithPath:@"doc4"] setData:@{@"key4" : @"value4"}]; + + // get docs and ensure they *are* from the cache, and matches the updated + // data. + FIRQuerySnapshot *result = [self readDocumentSetForRef:col source:FIRFirestoreSourceCache]; + XCTAssertTrue(result.metadata.fromCache); + XCTAssertTrue(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), (@[ + @{@"key1" : @"value1"}, @{@"key2" : @"value2", @"key2b" : @"value2b"}, + @{@"key3b" : @"value3b"}, @{@"key4" : @"value4"} + ])); + XCTAssertEqualObjects( + FIRQuerySnapshotGetDocChangesData(result), (@[ + @[ @(FIRDocumentChangeTypeAdded), @"doc1", @{@"key1" : @"value1"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc2", @{@"key2" : @"value2", @"key2b" : @"value2b"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc3", @{@"key3b" : @"value3b"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc4", @{@"key4" : @"value4"} ] + ])); +} + +- (void)testGetDocumentWhileOnlineServerOnly { + FIRDocumentReference *doc = [self documentRef]; + + // set document to a known value + NSDictionary *initialData = @{@"key" : @"value"}; + [self writeDocumentRef:doc data:initialData]; + + // get doc and ensure that it exists, is *not* from the cache, and matches + // the initialData. + FIRDocumentSnapshot *result = [self readDocumentForRef:doc source:FIRFirestoreSourceServer]; + XCTAssertTrue(result.exists); + XCTAssertFalse(result.metadata.fromCache); + XCTAssertFalse(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(result.data, initialData); +} + +- (void)testGetCollectionWhileOnlineServerOnly { + FIRCollectionReference *col = [self collectionRef]; + + // set a few documents to a known value + NSDictionary *> *initialDocs = @{ + @"doc1" : @{@"key1" : @"value1"}, + @"doc2" : @{@"key2" : @"value2"}, + @"doc3" : @{@"key3" : @"value3"}, + }; + [self writeAllDocuments:initialDocs toCollection:col]; + + // get docs and ensure they are *not* from the cache, and matches the + // initialData. + FIRQuerySnapshot *result = [self readDocumentSetForRef:col source:FIRFirestoreSourceServer]; + XCTAssertFalse(result.metadata.fromCache); + XCTAssertFalse(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), (@[ + @{@"key1" : @"value1"}, + @{@"key2" : @"value2"}, + @{@"key3" : @"value3"}, + ])); + XCTAssertEqualObjects(FIRQuerySnapshotGetDocChangesData(result), (@[ + @[ @(FIRDocumentChangeTypeAdded), @"doc1", @{@"key1" : @"value1"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc2", @{@"key2" : @"value2"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc3", @{@"key3" : @"value3"} ] + ])); +} + +- (void)testGetDocumentWhileOfflineServerOnly { + FIRDocumentReference *doc = [self documentRef]; + + // set document to a known value + NSDictionary *initialData = @{@"key1" : @"value1"}; + [self writeDocumentRef:doc data:initialData]; + + // go offline for the rest of this test + [self disableNetwork]; + + // attempt to get doc and ensure it cannot be retreived + XCTestExpectation *failedGetDocCompletion = [self expectationWithDescription:@"failedGetDoc"]; + [doc getDocumentWithSource:FIRFirestoreSourceServer + completion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeUnavailable); + [failedGetDocCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testGetCollectionWhileOfflineServerOnly { + FIRCollectionReference *col = [self collectionRef]; + + // set a few documents to a known value + NSDictionary *> *initialDocs = @{ + @"doc1" : @{@"key1" : @"value1"}, + @"doc2" : @{@"key2" : @"value2"}, + @"doc3" : @{@"key3" : @"value3"}, + }; + [self writeAllDocuments:initialDocs toCollection:col]; + + // go offline for the rest of this test + [self disableNetwork]; + + // attempt to get docs and ensure they cannot be retreived + XCTestExpectation *failedGetDocsCompletion = [self expectationWithDescription:@"failedGetDocs"]; + [col getDocumentsWithSource:FIRFirestoreSourceServer + completion:^(FIRQuerySnapshot *snapshot, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeUnavailable); + [failedGetDocsCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testGetDocumentWhileOfflineWithDifferentSource { + FIRDocumentReference *doc = [self documentRef]; + + // set document to a known value + NSDictionary *initialData = @{@"key1" : @"value1"}; + [self writeDocumentRef:doc data:initialData]; + + // go offline for the rest of this test + [self disableNetwork]; + + // update the doc (though don't wait for a server response. We're offline; so + // that ain't happening!). This allows us to further distinguished cached vs + // server responses below. + NSDictionary *newData = @{@"key2" : @"value2"}; + [doc setData:newData + completion:^(NSError *_Nullable error) { + XCTAssertTrue(false, "Because we're offline, this should never occur."); + }]; + + // Create an initial listener for this query (to attempt to disrupt the gets below) and wait for + // the listener to deliver its initial snapshot before continuing. + XCTestExpectation *listenerReady = [self expectationWithDescription:@"listenerReady"]; + [doc addSnapshotListener:^(FIRDocumentSnapshot *snapshot, NSError *error) { + [listenerReady fulfill]; + }]; + [self awaitExpectations]; + + // get doc (from cache) and ensure it exists, *is* from the cache, and + // matches the newData. + FIRDocumentSnapshot *result = [self readDocumentForRef:doc source:FIRFirestoreSourceCache]; + XCTAssertTrue(result.exists); + XCTAssertTrue(result.metadata.fromCache); + XCTAssertTrue(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(result.data, newData); + + // attempt to get doc (with default get source) + result = [self readDocumentForRef:doc source:FIRFirestoreSourceDefault]; + XCTAssertTrue(result.exists); + XCTAssertTrue(result.metadata.fromCache); + XCTAssertTrue(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(result.data, newData); + + // attempt to get doc (from the server) and ensure it cannot be retreived + XCTestExpectation *failedGetDocCompletion = [self expectationWithDescription:@"failedGetDoc"]; + [doc getDocumentWithSource:FIRFirestoreSourceServer + completion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeUnavailable); + [failedGetDocCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testGetCollectionWhileOfflineWithDifferentSource { + FIRCollectionReference *col = [self collectionRef]; + + // set a few documents to a known value + NSDictionary *> *initialDocs = @{ + @"doc1" : @{@"key1" : @"value1"}, + @"doc2" : @{@"key2" : @"value2"}, + @"doc3" : @{@"key3" : @"value3"}, + }; + [self writeAllDocuments:initialDocs toCollection:col]; + + // go offline for the rest of this test + [self disableNetwork]; + + // update the docs (though don't wait for a server response. We're offline; so + // that ain't happening!). This allows us to further distinguished cached vs + // server responses below. + [[col documentWithPath:@"doc2"] setData:@{@"key2b" : @"value2b"} options:FIRSetOptions.merge]; + [[col documentWithPath:@"doc3"] setData:@{@"key3b" : @"value3b"}]; + [[col documentWithPath:@"doc4"] setData:@{@"key4" : @"value4"}]; + + // Create an initial listener for this query (to attempt to disrupt the gets + // below) and wait for the listener to deliver its initial snapshot before + // continuing. + XCTestExpectation *listenerReady = [self expectationWithDescription:@"listenerReady"]; + [col addSnapshotListener:^(FIRQuerySnapshot *snapshot, NSError *error) { + [listenerReady fulfill]; + }]; + [self awaitExpectations]; + + // get docs (from cache) and ensure they *are* from the cache, and + // matches the updated data. + FIRQuerySnapshot *result = [self readDocumentSetForRef:col source:FIRFirestoreSourceCache]; + XCTAssertTrue(result.metadata.fromCache); + XCTAssertTrue(result.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), (@[ + @{@"key1" : @"value1"}, @{@"key2" : @"value2", @"key2b" : @"value2b"}, + @{@"key3b" : @"value3b"}, @{@"key4" : @"value4"} + ])); + XCTAssertEqualObjects( + FIRQuerySnapshotGetDocChangesData(result), (@[ + @[ @(FIRDocumentChangeTypeAdded), @"doc1", @{@"key1" : @"value1"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc2", @{@"key2" : @"value2", @"key2b" : @"value2b"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc3", @{@"key3b" : @"value3b"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc4", @{@"key4" : @"value4"} ] + ])); + + // attempt to get docs (with default get source) + result = [self readDocumentSetForRef:col source:FIRFirestoreSourceDefault]; + XCTAssertTrue(result.metadata.fromCache); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), (@[ + @{@"key1" : @"value1"}, @{@"key2" : @"value2", @"key2b" : @"value2b"}, + @{@"key3b" : @"value3b"}, @{@"key4" : @"value4"} + ])); + XCTAssertEqualObjects( + FIRQuerySnapshotGetDocChangesData(result), (@[ + @[ @(FIRDocumentChangeTypeAdded), @"doc1", @{@"key1" : @"value1"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc2", @{@"key2" : @"value2", @"key2b" : @"value2b"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc3", @{@"key3b" : @"value3b"} ], + @[ @(FIRDocumentChangeTypeAdded), @"doc4", @{@"key4" : @"value4"} ] + ])); + + // attempt to get docs (from the server) and ensure they cannot be retreived + XCTestExpectation *failedGetDocsCompletion = [self expectationWithDescription:@"failedGetDocs"]; + [col getDocumentsWithSource:FIRFirestoreSourceServer + completion:^(FIRQuerySnapshot *snapshot, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeUnavailable); + [failedGetDocsCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testGetNonExistingDocWhileOnlineWithDefaultSource { + FIRDocumentReference *doc = [self documentRef]; + + // get doc and ensure that it does not exist and is *not* from the cache. + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); + XCTAssertFalse(snapshot.metadata.fromCache); + XCTAssertFalse(snapshot.metadata.hasPendingWrites); +} + +- (void)testGetNonExistingCollectionWhileOnlineWithDefaultSource { + FIRCollectionReference *col = [self collectionRef]; + + // get collection and ensure it's empty and that it's *not* from the cache. + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:col]; + XCTAssertEqual(snapshot.count, 0); + XCTAssertEqual(snapshot.documentChanges.count, 0); + XCTAssertFalse(snapshot.metadata.fromCache); + XCTAssertFalse(snapshot.metadata.hasPendingWrites); +} + +- (void)testGetNonExistingDocWhileOfflineWithDefaultSource { + FIRDocumentReference *doc = [self documentRef]; + + // go offline for the rest of this test + [self disableNetwork]; + + // attempt to get doc. Currently, this is expected to fail. In the future, we + // might consider adding support for negative cache hits so that we know + // certain documents *don't* exist. + XCTestExpectation *getNonExistingDocCompletion = + [self expectationWithDescription:@"getNonExistingDoc"]; + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeUnavailable); + [getNonExistingDocCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testGetNonExistingCollectionWhileOfflineWithDefaultSource { + FIRCollectionReference *col = [self collectionRef]; + + // go offline for the rest of this test + [self disableNetwork]; + + // get collection and ensure it's empty and that it *is* from the cache. + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:col]; + XCTAssertEqual(snapshot.count, 0); + XCTAssertEqual(snapshot.documentChanges.count, 0); + XCTAssertTrue(snapshot.metadata.fromCache); + XCTAssertFalse(snapshot.metadata.hasPendingWrites); +} + +- (void)testGetNonExistingDocWhileOnlineCacheOnly { + FIRDocumentReference *doc = [self documentRef]; + + // attempt to get doc. Currently, this is expected to fail. In the future, we + // might consider adding support for negative cache hits so that we know + // certain documents *don't* exist. + XCTestExpectation *getNonExistingDocCompletion = + [self expectationWithDescription:@"getNonExistingDoc"]; + [doc getDocumentWithSource:FIRFirestoreSourceCache + completion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeUnavailable); + [getNonExistingDocCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testGetNonExistingCollectionWhileOnlineCacheOnly { + FIRCollectionReference *col = [self collectionRef]; + + // get collection and ensure it's empty and that it *is* from the cache. + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:col source:FIRFirestoreSourceCache]; + XCTAssertEqual(snapshot.count, 0); + XCTAssertEqual(snapshot.documentChanges.count, 0); + XCTAssertTrue(snapshot.metadata.fromCache); + XCTAssertFalse(snapshot.metadata.hasPendingWrites); +} + +- (void)testGetNonExistingDocWhileOfflineCacheOnly { + FIRDocumentReference *doc = [self documentRef]; + + // go offline for the rest of this test + [self disableNetwork]; + + // attempt to get doc. Currently, this is expected to fail. In the future, we + // might consider adding support for negative cache hits so that we know + // certain documents *don't* exist. + XCTestExpectation *getNonExistingDocCompletion = + [self expectationWithDescription:@"getNonExistingDoc"]; + [doc getDocumentWithSource:FIRFirestoreSourceCache + completion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeUnavailable); + [getNonExistingDocCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testGetNonExistingCollectionWhileOfflineCacheOnly { + FIRCollectionReference *col = [self collectionRef]; + + // go offline for the rest of this test + [self disableNetwork]; + + // get collection and ensure it's empty and that it *is* from the cache. + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:col source:FIRFirestoreSourceCache]; + XCTAssertEqual(snapshot.count, 0); + XCTAssertEqual(snapshot.documentChanges.count, 0); + XCTAssertTrue(snapshot.metadata.fromCache); + XCTAssertFalse(snapshot.metadata.hasPendingWrites); +} + +- (void)testGetNonExistingDocWhileOnlineServerOnly { + FIRDocumentReference *doc = [self documentRef]; + + // get doc and ensure that it does not exist and is *not* from the cache. + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc source:FIRFirestoreSourceServer]; + XCTAssertFalse(snapshot.exists); + XCTAssertFalse(snapshot.metadata.fromCache); + XCTAssertFalse(snapshot.metadata.hasPendingWrites); +} + +- (void)testGetNonExistingCollectionWhileOnlineServerOnly { + FIRCollectionReference *col = [self collectionRef]; + + // get collection and ensure that it's empty and that it's *not* from the cache. + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:col source:FIRFirestoreSourceServer]; + XCTAssertEqual(snapshot.count, 0); + XCTAssertEqual(snapshot.documentChanges.count, 0); + XCTAssertFalse(snapshot.metadata.fromCache); + XCTAssertFalse(snapshot.metadata.hasPendingWrites); +} + +- (void)testGetNonExistingDocWhileOfflineServerOnly { + FIRDocumentReference *doc = [self documentRef]; + + // go offline for the rest of this test + [self disableNetwork]; + + // attempt to get doc. Currently, this is expected to fail. In the future, we + // might consider adding support for negative cache hits so that we know + // certain documents *don't* exist. + XCTestExpectation *getNonExistingDocCompletion = + [self expectationWithDescription:@"getNonExistingDoc"]; + [doc getDocumentWithSource:FIRFirestoreSourceServer + completion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeUnavailable); + [getNonExistingDocCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testGetNonExistingCollectionWhileOfflineServerOnly { + FIRCollectionReference *col = [self collectionRef]; + + // go offline for the rest of this test + [self disableNetwork]; + + // attempt to get collection and ensure that it cannot be retreived + XCTestExpectation *failedGetDocsCompletion = [self expectationWithDescription:@"failedGetDocs"]; + [col getDocumentsWithSource:FIRFirestoreSourceServer + completion:^(FIRQuerySnapshot *snapshot, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeUnavailable); + [failedGetDocsCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h index e27491b9dfc..585d99a15d7 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h @@ -20,6 +20,8 @@ #import "Firestore/Example/Tests/Util/XCTestCase+Await.h" #import "Firestore/Source/Core/FSTTypes.h" +#import "FIRFirestoreSource.h" + @class FIRCollectionReference; @class FIRDocumentSnapshot; @class FIRDocumentReference; @@ -71,8 +73,13 @@ extern "C" { - (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref; +- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref + source:(FIRFirestoreSource)source; + - (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query; +- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query source:(FIRFirestoreSource)source; + - (FIRDocumentSnapshot *)readSnapshotForRef:(FIRDocumentReference *)query requireOnline:(BOOL)online; @@ -108,6 +115,10 @@ NSArray *> *FIRQuerySnapshotGetData(FIRQuerySnapsho /** Converts the FIRQuerySnapshot to an NSArray containing the document IDs in order. */ NSArray *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs); +/** Converts the FIRQuerySnapshot to an NSArray containing an NSArray containing the doc change data + * in order of { type, doc title, doc data }. */ +NSArray *> *FIRQuerySnapshotGetDocChangesData(FIRQuerySnapshot *docs); + #if __cplusplus } // extern "C" #endif diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm index 9bfdb3b97f8..1817015df8c 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm @@ -212,28 +212,39 @@ - (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path, } - (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref { + return [self readDocumentForRef:ref source:FIRFirestoreSourceDefault]; +} + +- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref + source:(FIRFirestoreSource)source { __block FIRDocumentSnapshot *result; XCTestExpectation *expectation = [self expectationWithDescription:@"getData"]; - [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *doc, NSError *_Nullable error) { - XCTAssertNil(error); - result = doc; - [expectation fulfill]; - }]; + [ref getDocumentWithSource:source + completion:^(FIRDocumentSnapshot *doc, NSError *_Nullable error) { + XCTAssertNil(error); + result = doc; + [expectation fulfill]; + }]; [self awaitExpectations]; return result; } - (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query { + return [self readDocumentSetForRef:query source:FIRFirestoreSourceDefault]; +} + +- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query source:(FIRFirestoreSource)source { __block FIRQuerySnapshot *result; XCTestExpectation *expectation = [self expectationWithDescription:@"getData"]; - [query getDocumentsWithCompletion:^(FIRQuerySnapshot *documentSet, NSError *error) { - XCTAssertNil(error); - result = documentSet; - [expectation fulfill]; - }]; + [query getDocumentsWithSource:source + completion:^(FIRQuerySnapshot *documentSet, NSError *error) { + XCTAssertNil(error); + result = documentSet; + [expectation fulfill]; + }]; [self awaitExpectations]; return result; @@ -329,6 +340,18 @@ - (void)waitUntil:(BOOL (^)())predicate { return result; } +extern "C" NSArray *> *FIRQuerySnapshotGetDocChangesData(FIRQuerySnapshot *docs) { + NSMutableArray *> *result = [NSMutableArray array]; + for (FIRDocumentChange *docChange in docs.documentChanges) { + NSMutableArray *docChangeData = [NSMutableArray array]; + [docChangeData addObject:@(docChange.type)]; + [docChangeData addObject:docChange.document.documentID]; + [docChangeData addObject:docChange.document.data]; + [result addObject:docChangeData]; + } + return result; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm index 24234726cf8..c2fc546fe4e 100644 --- a/Firestore/Source/API/FIRDocumentReference.mm +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -22,6 +22,7 @@ #include #import "FIRFirestoreErrors.h" +#import "FIRFirestoreSource.h" #import "FIRSnapshotMetadata.h" #import "Firestore/Source/API/FIRCollectionReference+Internal.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" @@ -162,6 +163,17 @@ - (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error - (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, NSError *_Nullable error))completion { + return [self getDocumentWithSource:FIRFirestoreSourceDefault completion:completion]; +} + +- (void)getDocumentWithSource:(FIRFirestoreSource)source + completion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + if (source == FIRFirestoreSourceCache) { + [self.firestore.client getDocumentFromLocalCache:self completion:completion]; + return; + } + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES includeDocumentMetadataChanges:YES waitForSyncWhenOnline:YES]; @@ -188,7 +200,6 @@ - (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable docum // offline. // 2) Actually call the completion handler with an error if the document doesn't exist when // you are offline. - // TODO(dimond): Use proper error domain completion(nil, [NSError errorWithDomain:FIRFirestoreErrorDomain code:FIRFirestoreErrorCodeUnavailable @@ -196,6 +207,18 @@ - (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable docum NSLocalizedDescriptionKey : @"Failed to get document because the client is offline.", }]); + } else if (snapshot.exists && snapshot.metadata.fromCache && + source == FIRFirestoreSourceServer) { + completion(nil, + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{ + NSLocalizedDescriptionKey : + @"Failed to get document from server. (However, this " + @"document does exist in the local cache. Run again " + @"without setting source to FIRFirestoreSourceServer to " + @"retrieve the cached document.)" + }]); } else { completion(snapshot, nil); } diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm index 14dcaefcb2c..2d78ac0099c 100644 --- a/Firestore/Source/API/FIRQuery.mm +++ b/Firestore/Source/API/FIRQuery.mm @@ -17,6 +17,8 @@ #import "FIRQuery.h" #import "FIRDocumentReference.h" +#import "FIRFirestoreErrors.h" +#import "FIRFirestoreSource.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" #import "Firestore/Source/API/FIRFieldPath+Internal.h" @@ -96,9 +98,21 @@ - (NSUInteger)hash { - (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error))completion { - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:YES]; + [self getDocumentsWithSource:FIRFirestoreSourceDefault completion:completion]; +} + +- (void)getDocumentsWithSource:(FIRFirestoreSource)source + completion:(void (^)(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error))completion { + if (source == FIRFirestoreSourceCache) { + [self.firestore.client getDocumentsFromLocalCache:self completion:completion]; + return; + } + + FSTListenOptions *listenOptions = + [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:YES]; dispatch_semaphore_t registered = dispatch_semaphore_create(0); __block id listenerRegistration; @@ -113,10 +127,24 @@ - (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapsho dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); [listenerRegistration remove]; - completion(snapshot, nil); + if (snapshot.metadata.fromCache && source == FIRFirestoreSourceServer) { + completion(nil, + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{ + NSLocalizedDescriptionKey : + @"Failed to get documents from server. (However, these " + @"documents may exist in the local cache. Run again " + @"without setting source to FIRFirestoreSourceServer to " + @"retrieve the cached documents.)" + }]); + } else { + completion(snapshot, nil); + } }; - listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener]; + listenerRegistration = + [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; dispatch_semaphore_signal(registered); } diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h index 6da5ed37665..7285e65ae9b 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.h +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -24,6 +24,12 @@ #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" +@class FIRDocumentReference; +@class FIRDocumentSnapshot; +@class FIRQuery; +@class FIRQuerySnapshot; +@class FSTDatabaseID; +@class FSTDatabaseInfo; @class FSTDispatchQueue; @class FSTDocument; @class FSTListenOptions; @@ -72,6 +78,22 @@ NS_ASSUME_NONNULL_BEGIN /** Stops listening to a query previously listened to. */ - (void)removeListener:(FSTQueryListener *)listener; +/** + * Retrieves a document from the cache via the indicated completion. If the doc + * doesn't exist, an error will be sent to the completion. + */ +- (void)getDocumentFromLocalCache:(FIRDocumentReference *)doc + completion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion; + +/** + * Retrieves a (possibly empty) set of documents from the cache via the + * indicated completion. + */ +- (void)getDocumentsFromLocalCache:(FIRQuery *)query + completion:(void (^)(FIRQuerySnapshot *_Nullable query, + NSError *_Nullable error))completion; + /** Write mutations. completion will be notified when it's written to the backend. */ - (void)writeMutations:(NSArray *)mutations completion:(nullable FSTVoidErrorBlock)completion; diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm index 33d1903b205..4f1a20b3e92 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -19,15 +19,25 @@ #include // NOLINT(build/c++11) #include +#import "FIRFirestoreErrors.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" #import "Firestore/Source/Core/FSTEventManager.h" +#import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Core/FSTSyncEngine.h" #import "Firestore/Source/Core/FSTTransaction.h" +#import "Firestore/Source/Core/FSTView.h" #import "Firestore/Source/Local/FSTEagerGarbageCollector.h" #import "Firestore/Source/Local/FSTLevelDB.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Remote/FSTDatastore.h" #import "Firestore/Source/Remote/FSTRemoteStore.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" @@ -271,6 +281,59 @@ - (void)removeListener:(FSTQueryListener *)listener { }]; } +- (void)getDocumentFromLocalCache:(FIRDocumentReference *)doc + completion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + [self.workerDispatchQueue dispatchAsync:^{ + FSTMaybeDocument *maybeDoc = [self.localStore readDocument:doc.key]; + if (maybeDoc) { + completion([FIRDocumentSnapshot snapshotWithFirestore:doc.firestore + documentKey:doc.key + document:(FSTDocument *)maybeDoc + fromCache:YES], + nil); + } else { + completion(nil, + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{ + NSLocalizedDescriptionKey : + @"Failed to get document from cache. (However, this " + @"document may exist on the server. Run again without " + @"setting source to FIRFirestoreSourceCache to attempt to " + @"retrieve the document from the server.)", + }]); + } + }]; +} + +- (void)getDocumentsFromLocalCache:(FIRQuery *)query + 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]; + FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs]; + FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; + FSTAssert(viewChange.limboChanges.count == 0, + @"View returned limbo documents during local-only query execution."); + + FSTViewSnapshot *snapshot = viewChange.snapshot; + FIRSnapshotMetadata *metadata = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites + fromCache:snapshot.fromCache]; + + completion([FIRQuerySnapshot snapshotWithFirestore:query.firestore + originalQuery:query.query + snapshot:snapshot + metadata:metadata], + nil); + }]; +} + - (void)writeMutations:(NSArray *)mutations completion:(nullable FSTVoidErrorBlock)completion { [self.workerDispatchQueue dispatchAsync:^{ diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h index e7ba6ebe8a4..4aa8c45ae80 100644 --- a/Firestore/Source/Public/FIRDocumentReference.h +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -16,6 +16,7 @@ #import +#import "FIRFirestoreSource.h" #import "FIRListenerRegistration.h" @class FIRCollectionReference; @@ -166,11 +167,29 @@ NS_SWIFT_NAME(DocumentReference) /** * Reads the document referenced by this `FIRDocumentReference`. * + * This method attempts to provide up-to-date data when possible by waiting for + * data from the server, but it may return cached data or fail if you are + * offline and the server cannot be reached. See the + * `getDocument(source:completion:)` method to change this behavior. + * * @param completion a block to execute once the document has been successfully read. */ - (void)getDocumentWithCompletion:(FIRDocumentSnapshotBlock)completion NS_SWIFT_NAME(getDocument(completion:)); +/** + * Reads the document referenced by this `FIRDocumentReference`. + * + * @param source indicates whether the results should be fetched from the cache + * only (`Source.cache`), the server only (`Source.server`), or to attempt + * the server and fall back to the cache (`Source.default`). + * @param completion a block to execute once the document has been successfully read. + */ +// clang-format off +- (void)getDocumentWithSource:(FIRFirestoreSource)source completion:(FIRDocumentSnapshotBlock)completion + NS_SWIFT_NAME(getDocument(source:completion:)); +// clang-format on + /** * Attaches a listener for DocumentSnapshot events. * diff --git a/Firestore/Source/Public/FIRFirestoreSource.h b/Firestore/Source/Public/FIRFirestoreSource.h new file mode 100644 index 00000000000..c1337475ffa --- /dev/null +++ b/Firestore/Source/Public/FIRFirestoreSource.h @@ -0,0 +1,48 @@ +/* + * 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. + */ + +#import + +/** + * An enum that configures the behavior of `DocumentReference.getDocument()` and + * `Query.getDocuments()`. By providing a source enum the `getDocument[s]` + * methods can be configured to fetch results only from the server, only from + * the local cache, or attempt to fetch results from the server and fall back to + * the cache (which is the default). + * + * Setting the source to `Source.default` causes Firestore to try to retrieve an + * up-to-date (server-retrieved) snapshot, but fall back to returning cached + * data if the server can't be reached. + * + * Setting the source to `Source.server` causes Firestore to avoid the cache, + * generating an error if the server cannot be reached. Note that the cache will + * still be updated if the server request succeeds. Also note that + * latency-compensation still takes effect, so any pending write operations will + * be visible in the returned data (merged into the server-provided data). + * + * Setting the source to `Source.cache` causes Firestore to immediately return a + * value from the cache, ignoring the server completely (implying that the + * returned value may be stale with respect to the value on the server). If + * there is no data in the cache to satisfy the `getDocument[s]` call, + * `DocumentReference.getDocument()` will return an error and + * `QuerySnapshot.getDocuments()` will return an empty `QuerySnapshot` with no + * documents. + */ +typedef NS_ENUM(NSUInteger, FIRFirestoreSource) { + FIRFirestoreSourceDefault, + FIRFirestoreSourceServer, + FIRFirestoreSourceCache +} NS_SWIFT_NAME(FirestoreSource); diff --git a/Firestore/Source/Public/FIRQuery.h b/Firestore/Source/Public/FIRQuery.h index a28af3948f6..946cf06bd35 100644 --- a/Firestore/Source/Public/FIRQuery.h +++ b/Firestore/Source/Public/FIRQuery.h @@ -16,6 +16,7 @@ #import +#import "FIRFirestoreSource.h" #import "FIRListenerRegistration.h" @class FIRFieldPath; @@ -44,12 +45,31 @@ NS_SWIFT_NAME(Query) /** * Reads the documents matching this query. * + * This method attempts to provide up-to-date data when possible by waiting for + * data from the server, but it may return cached data or fail if you are + * offline and the server cannot be reached. See the + * `getDocuments(source:completion:)` method to change this behavior. + * * @param completion a block to execute once the documents have been successfully read. * documentSet will be `nil` only if error is `non-nil`. */ - (void)getDocumentsWithCompletion:(FIRQuerySnapshotBlock)completion NS_SWIFT_NAME(getDocuments(completion:)); +/** + * Reads the documents matching this query. + * + * @param source indicates whether the results should be fetched from the cache + * only (`Source.cache`), the server only (`Source.server`), or to attempt + * the server and fall back to the cache (`Source.default`). + * @param completion a block to execute once the documents have been successfully read. + * documentSet will be `nil` only if error is `non-nil`. + */ +// clang-format off +- (void)getDocumentsWithSource:(FIRFirestoreSource)source completion:(FIRQuerySnapshotBlock)completion + NS_SWIFT_NAME(getDocuments(source:completion:)); +// clang-format on + /** * Attaches a listener for QuerySnapshot events. *