aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Firestore/CHANGELOG.md4
-rw-r--r--Firestore/Example/Firestore.xcodeproj/project.pbxproj4
-rw-r--r--Firestore/Example/SwiftBuildTest/main.swift20
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRFirestoreSourceTests.mm648
-rw-r--r--Firestore/Example/Tests/Util/FSTIntegrationTestCase.h11
-rw-r--r--Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm43
-rw-r--r--Firestore/Source/API/FIRDocumentReference.mm25
-rw-r--r--Firestore/Source/API/FIRQuery.mm38
-rw-r--r--Firestore/Source/Core/FSTFirestoreClient.h22
-rw-r--r--Firestore/Source/Core/FSTFirestoreClient.mm63
-rw-r--r--Firestore/Source/Public/FIRDocumentReference.h19
-rw-r--r--Firestore/Source/Public/FIRFirestoreSource.h48
-rw-r--r--Firestore/Source/Public/FIRQuery.h20
13 files changed, 949 insertions, 16 deletions
diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md
index 9f53593..77d823a 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 d20dc48..dd64351 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 = "<group>"; };
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>"; };
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>"; };
@@ -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 00839c4..f61da2b 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 0000000..0f844c6
--- /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 <FirebaseFirestore/FirebaseFirestore.h>
+
+#import <XCTest/XCTest.h>
+
+#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<NSString *, id> *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<NSString *, NSDictionary<NSString *, id> *> *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<NSString *, id> *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<NSString *, id> *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<NSString *, NSDictionary<NSString *, id> *> *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<NSString *, id> *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<NSString *, NSDictionary<NSString *, id> *> *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<NSString *, id> *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<NSString *, id> *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<NSString *, NSDictionary<NSString *, id> *> *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<NSString *, id> *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<NSString *, NSDictionary<NSString *, id> *> *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<NSString *, id> *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<NSString *, NSDictionary<NSString *, id> *> *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<NSString *, id> *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<NSString *, id> *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<NSString *, NSDictionary<NSString *, id> *> *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 e27491b..585d99a 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<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(FIRQuerySnapsho
/** Converts the FIRQuerySnapshot to an NSArray containing the document IDs in order. */
NSArray<NSString *> *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<NSArray<id> *> *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 9bfdb3b..1817015 100644
--- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm
+++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm
@@ -212,28 +212,39 @@ NS_ASSUME_NONNULL_BEGIN
}
- (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 @@ extern "C" NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs) {
return result;
}
+extern "C" NSArray<NSArray<id> *> *FIRQuerySnapshotGetDocChangesData(FIRQuerySnapshot *docs) {
+ NSMutableArray<NSMutableArray<id> *> *result = [NSMutableArray array];
+ for (FIRDocumentChange *docChange in docs.documentChanges) {
+ NSMutableArray<id> *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 2423472..c2fc546 100644
--- a/Firestore/Source/API/FIRDocumentReference.mm
+++ b/Firestore/Source/API/FIRDocumentReference.mm
@@ -22,6 +22,7 @@
#include <utility>
#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 @@ NS_ASSUME_NONNULL_BEGIN
- (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 @@ NS_ASSUME_NONNULL_BEGIN
// 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 @@ NS_ASSUME_NONNULL_BEGIN
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 14dcaef..2d78ac0 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 @@ NS_ASSUME_NONNULL_BEGIN
- (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<FIRListenerRegistration> listenerRegistration;
@@ -113,10 +127,24 @@ NS_ASSUME_NONNULL_BEGIN
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 6da5ed3..7285e65 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<FSTMutation *> *)mutations
completion:(nullable FSTVoidErrorBlock)completion;
diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm
index 33d1903..4f1a20b 100644
--- a/Firestore/Source/Core/FSTFirestoreClient.mm
+++ b/Firestore/Source/Core/FSTFirestoreClient.mm
@@ -19,15 +19,25 @@
#include <future> // NOLINT(build/c++11)
#include <memory>
+#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 @@ NS_ASSUME_NONNULL_BEGIN
}];
}
+- (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<FSTMutation *> *)mutations
completion:(nullable FSTVoidErrorBlock)completion {
[self.workerDispatchQueue dispatchAsync:^{
diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h
index e7ba6eb..4aa8c45 100644
--- a/Firestore/Source/Public/FIRDocumentReference.h
+++ b/Firestore/Source/Public/FIRDocumentReference.h
@@ -16,6 +16,7 @@
#import <Foundation/Foundation.h>
+#import "FIRFirestoreSource.h"
#import "FIRListenerRegistration.h"
@class FIRCollectionReference;
@@ -166,12 +167,30 @@ 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.
*
* @param listener The listener to attach.
diff --git a/Firestore/Source/Public/FIRFirestoreSource.h b/Firestore/Source/Public/FIRFirestoreSource.h
new file mode 100644
index 0000000..c133747
--- /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 <Foundation/Foundation.h>
+
+/**
+ * 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 a28af39..946cf06 100644
--- a/Firestore/Source/Public/FIRQuery.h
+++ b/Firestore/Source/Public/FIRQuery.h
@@ -16,6 +16,7 @@
#import <Foundation/Foundation.h>
+#import "FIRFirestoreSource.h"
#import "FIRListenerRegistration.h"
@class FIRFieldPath;
@@ -44,6 +45,11 @@ 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`.
*/
@@ -51,6 +57,20 @@ NS_SWIFT_NAME(Query)
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.
*
* @param listener The listener to attach.