aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar rsgowman <rgowman@google.com>2018-04-18 10:30:37 -0400
committerGravatar GitHub <noreply@github.com>2018-04-18 10:30:37 -0400
commita5b3185ed372fc16b5430c230c197e3dbd34f953 (patch)
tree8f77610cf27ee35f5cccad825b6a33e74d7be8ef
parent6a39c71be83f589550c7dfa3c6d89d8b6145887c (diff)
Add GetOptions for controlling offline get behaviour (#655)
Add option to allow the user to control where DocumentReference.getDocument() and CollectionReference.getDocuments() fetches from. By default, it fetches from the server (if possible) and falls back to the local cache. It's now possible to alternatively fetch from the local cache only, or to fetch from the server only (though in the server only case, latency compensation is still enabled).
-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.