Skip to content

Commit 8b29658

Browse files
authored
GetOptions for controlling offline behaviour (#463)
Add option to allow the user to control where DocumentReference.get() and Query.get() 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).
1 parent 27a77fd commit 8b29658

File tree

8 files changed

+860
-63
lines changed

8 files changed

+860
-63
lines changed

packages/firebase/index.d.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,37 @@ declare namespace firebase.firestore {
11121112
readonly merge?: boolean;
11131113
}
11141114

1115+
/**
1116+
* An options object that configures the behavior of `get()` calls on
1117+
* `DocumentReference` and `Query`. By providing a `GetOptions` object, these
1118+
* methods can be configured to fetch results only from the server, only from
1119+
* the local cache or attempt to fetch results from the server and fall back to
1120+
* the cache (which is the default).
1121+
*/
1122+
export interface GetOptions {
1123+
/**
1124+
* Describes whether we should get from server or cache.
1125+
*
1126+
* Setting to 'default' (or not setting at all), causes Firestore to try to
1127+
* retrieve an up-to-date (server-retrieved) snapshot, but fall back to
1128+
* returning cached data if the server can't be reached.
1129+
*
1130+
* Setting to 'server' causes Firestore to avoid the cache, generating an
1131+
* error if the server cannot be reached. Note that the cache will still be
1132+
* updated if the server request succeeds. Also note that latency-compensation
1133+
* still takes effect, so any pending write operations will be visible in the
1134+
* returned data (merged into the server-provided data).
1135+
*
1136+
* Setting to 'cache' causes Firestore to immediately return a value from the
1137+
* cache, ignoring the server completely (implying that the returned value
1138+
* may be stale with respect to the value on the server.) If there is no data
1139+
* in the cache to satisfy the `get()` call, `DocumentReference.get()` will
1140+
* return an error and `QuerySnapshot.get()` will return an empty
1141+
* `QuerySnapshot` with no documents.
1142+
*/
1143+
readonly source?: 'default' | 'server' | 'cache';
1144+
}
1145+
11151146
/**
11161147
* A `DocumentReference` refers to a document location in a Firestore database
11171148
* and can be used to write, read, or listen to the location. The document at
@@ -1213,14 +1244,16 @@ declare namespace firebase.firestore {
12131244
/**
12141245
* Reads the document referred to by this `DocumentReference`.
12151246
*
1216-
* Note: get() attempts to provide up-to-date data when possible by waiting
1217-
* for data from the server, but it may return cached data or fail if you
1218-
* are offline and the server cannot be reached.
1247+
* Note: By default, get() attempts to provide up-to-date data when possible
1248+
* by waiting for data from the server, but it may return cached data or fail
1249+
* if you are offline and the server cannot be reached. This behavior can be
1250+
* altered via the `GetOptions` parameter.
12191251
*
1252+
* @param options An object to configure the get behavior.
12201253
* @return A Promise resolved with a DocumentSnapshot containing the
12211254
* current document contents.
12221255
*/
1223-
get(): Promise<DocumentSnapshot>;
1256+
get(options?: GetOptions): Promise<DocumentSnapshot>;
12241257

12251258
/**
12261259
* Attaches a listener for DocumentSnapshot events. You may either pass
@@ -1598,9 +1631,15 @@ declare namespace firebase.firestore {
15981631
/**
15991632
* Executes the query and returns the results as a QuerySnapshot.
16001633
*
1634+
* Note: By default, get() attempts to provide up-to-date data when possible
1635+
* by waiting for data from the server, but it may return cached data or fail
1636+
* if you are offline and the server cannot be reached. This behavior can be
1637+
* altered via the `GetOptions` parameter.
1638+
*
1639+
* @param options An object to configure the get behavior.
16011640
* @return A Promise that will be resolved with the results of the Query.
16021641
*/
1603-
get(): Promise<QuerySnapshot>;
1642+
get(options?: GetOptions): Promise<QuerySnapshot>;
16041643

16051644
/**
16061645
* Attaches a listener for QuerySnapshot events. You may either pass

packages/firestore-types/index.d.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,37 @@ export interface SetOptions {
494494
readonly merge?: boolean;
495495
}
496496

497+
/**
498+
* An options object that configures the behavior of `get()` calls on
499+
* `DocumentReference` and `Query`. By providing a `GetOptions` object, these
500+
* methods can be configured to fetch results only from the server, only from
501+
* the local cache or attempt to fetch results from the server and fall back to
502+
* the cache (which is the default).
503+
*/
504+
export interface GetOptions {
505+
/**
506+
* Describes whether we should get from server or cache.
507+
*
508+
* Setting to 'default' (or not setting at all), causes Firestore to try to
509+
* retrieve an up-to-date (server-retrieved) snapshot, but fall back to
510+
* returning cached data if the server can't be reached.
511+
*
512+
* Setting to 'server' causes Firestore to avoid the cache, generating an
513+
* error if the server cannot be reached. Note that the cache will still be
514+
* updated if the server request succeeds. Also note that latency-compensation
515+
* still takes effect, so any pending write operations will be visible in the
516+
* returned data (merged into the server-provided data).
517+
*
518+
* Setting to 'cache' causes Firestore to immediately return a value from the
519+
* cache, ignoring the server completely (implying that the returned value
520+
* may be stale with respect to the value on the server.) If there is no data
521+
* in the cache to satisfy the `get()` call, `DocumentReference.get()` will
522+
* return an error and `QuerySnapshot.get()` will return an empty
523+
* `QuerySnapshot` with no documents.
524+
*/
525+
readonly source?: 'default' | 'server' | 'cache';
526+
}
527+
497528
/**
498529
* A `DocumentReference` refers to a document location in a Firestore database
499530
* and can be used to write, read, or listen to the location. The document at
@@ -595,14 +626,16 @@ export class DocumentReference {
595626
/**
596627
* Reads the document referred to by this `DocumentReference`.
597628
*
598-
* Note: get() attempts to provide up-to-date data when possible by waiting
599-
* for data from the server, but it may return cached data or fail if you
600-
* are offline and the server cannot be reached.
629+
* Note: By default, get() attempts to provide up-to-date data when possible
630+
* by waiting for data from the server, but it may return cached data or fail
631+
* if you are offline and the server cannot be reached. This behavior can be
632+
* altered via the `GetOptions` parameter.
601633
*
634+
* @param options An object to configure the get behavior.
602635
* @return A Promise resolved with a DocumentSnapshot containing the
603636
* current document contents.
604637
*/
605-
get(): Promise<DocumentSnapshot>;
638+
get(options?: GetOptions): Promise<DocumentSnapshot>;
606639

607640
/**
608641
* Attaches a listener for DocumentSnapshot events. You may either pass
@@ -976,9 +1009,15 @@ export class Query {
9761009
/**
9771010
* Executes the query and returns the results as a QuerySnapshot.
9781011
*
1012+
* Note: By default, get() attempts to provide up-to-date data when possible
1013+
* by waiting for data from the server, but it may return cached data or fail
1014+
* if you are offline and the server cannot be reached. This behavior can be
1015+
* altered via the `GetOptions` parameter.
1016+
*
1017+
* @param options An object to configure the get behavior.
9791018
* @return A Promise that will be resolved with the results of the Query.
9801019
*/
981-
get(): Promise<QuerySnapshot>;
1020+
get(options?: GetOptions): Promise<QuerySnapshot>;
9821021

9831022
/**
9841023
* Attaches a listener for QuerySnapshot events. You may either pass

packages/firestore/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
`FirestoreSettings` to `true`. Note that the current behavior
1111
(`DocumentSnapshot`s returning JS Date objects) will be removed in a future
1212
release. `Timestamp` supports higher precision than JS Date.
13+
- [feature] Added ability to control whether DocumentReference.get() and
14+
Query.get() should fetch from server only, (by passing { source: 'server' }),
15+
cache only (by passing { source: 'cache' }), or attempt server and fall back
16+
to the cache (which was the only option previously, and is now the default).
1317

1418
# 0.3.6
1519
- [fixed] Fixed a regression in the Firebase JS release 4.11.0 that could

packages/firestore/src/api/database.ts

Lines changed: 133 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,43 +1000,91 @@ export class DocumentReference implements firestore.DocumentReference {
10001000
};
10011001
}
10021002

1003-
get(): Promise<firestore.DocumentSnapshot> {
1004-
validateExactNumberOfArgs('DocumentReference.get', arguments, 0);
1003+
get(options?: firestore.GetOptions): Promise<firestore.DocumentSnapshot> {
1004+
validateOptionNames('DocumentReference.get', options, ['source']);
1005+
if (options) {
1006+
validateNamedOptionalPropertyEquals(
1007+
'DocumentReference.get',
1008+
'options',
1009+
'source',
1010+
options.source,
1011+
['default', 'server', 'cache']
1012+
);
1013+
}
10051014
return new Promise(
10061015
(resolve: Resolver<firestore.DocumentSnapshot>, reject: Rejecter) => {
1007-
const unlisten = this.onSnapshotInternal(
1008-
{
1009-
includeQueryMetadataChanges: true,
1010-
includeDocumentMetadataChanges: true,
1011-
waitForSyncWhenOnline: true
1012-
},
1013-
{
1014-
next: (snap: firestore.DocumentSnapshot) => {
1015-
// Remove query first before passing event to user to avoid
1016-
// user actions affecting the now stale query.
1017-
unlisten();
1018-
1019-
if (!snap.exists && snap.metadata.fromCache) {
1020-
// TODO(dimond): If we're online and the document doesn't
1021-
// exist then we resolve with a doc.exists set to false. If
1022-
// we're offline however, we reject the Promise in this
1023-
// case. Two options: 1) Cache the negative response from
1024-
// the server so we can deliver that even when you're
1025-
// offline 2) Actually reject the Promise in the online case
1026-
// if the document doesn't exist.
1027-
reject(
1028-
new FirestoreError(
1029-
Code.ABORTED,
1030-
'Failed to get document because the client is ' + 'offline.'
1031-
)
1032-
);
1033-
} else {
1034-
resolve(snap);
1035-
}
1036-
},
1037-
error: reject
1016+
if (options && options.source === 'cache') {
1017+
this.firestore
1018+
.ensureClientConfigured()
1019+
.getDocumentFromLocalCache(this._key)
1020+
.then((doc: Document) => {
1021+
resolve(
1022+
new DocumentSnapshot(
1023+
this.firestore,
1024+
this._key,
1025+
doc,
1026+
/*fromCache=*/ true
1027+
)
1028+
);
1029+
}, reject);
1030+
} else {
1031+
this.getViaSnapshotListener(resolve, reject, options);
1032+
}
1033+
}
1034+
);
1035+
}
1036+
1037+
private getViaSnapshotListener(
1038+
resolve: Resolver<firestore.DocumentSnapshot>,
1039+
reject: Rejecter,
1040+
options?: firestore.GetOptions
1041+
): void {
1042+
const unlisten = this.onSnapshotInternal(
1043+
{
1044+
includeQueryMetadataChanges: true,
1045+
includeDocumentMetadataChanges: true,
1046+
waitForSyncWhenOnline: true
1047+
},
1048+
{
1049+
next: (snap: firestore.DocumentSnapshot) => {
1050+
// Remove query first before passing event to user to avoid
1051+
// user actions affecting the now stale query.
1052+
unlisten();
1053+
1054+
if (!snap.exists && snap.metadata.fromCache) {
1055+
// TODO(dimond): If we're online and the document doesn't
1056+
// exist then we resolve with a doc.exists set to false. If
1057+
// we're offline however, we reject the Promise in this
1058+
// case. Two options: 1) Cache the negative response from
1059+
// the server so we can deliver that even when you're
1060+
// offline 2) Actually reject the Promise in the online case
1061+
// if the document doesn't exist.
1062+
reject(
1063+
new FirestoreError(
1064+
Code.UNAVAILABLE,
1065+
'Failed to get document because the client is ' + 'offline.'
1066+
)
1067+
);
1068+
} else if (
1069+
snap.exists &&
1070+
snap.metadata.fromCache &&
1071+
options &&
1072+
options.source === 'server'
1073+
) {
1074+
reject(
1075+
new FirestoreError(
1076+
Code.UNAVAILABLE,
1077+
'Failed to get document from server. (However, this ' +
1078+
'document does exist in the local cache. Run again ' +
1079+
'without setting source to "server" to ' +
1080+
'retrieve the cached document.)'
1081+
)
1082+
);
1083+
} else {
1084+
resolve(snap);
10381085
}
1039-
);
1086+
},
1087+
error: reject
10401088
}
10411089
);
10421090
}
@@ -1619,27 +1667,60 @@ export class Query implements firestore.Query {
16191667
};
16201668
}
16211669

1622-
get(): Promise<firestore.QuerySnapshot> {
1623-
validateExactNumberOfArgs('Query.get', arguments, 0);
1670+
get(options?: firestore.GetOptions): Promise<firestore.QuerySnapshot> {
1671+
validateBetweenNumberOfArgs('Query.get', arguments, 0, 1);
16241672
return new Promise(
16251673
(resolve: Resolver<firestore.QuerySnapshot>, reject: Rejecter) => {
1626-
const unlisten = this.onSnapshotInternal(
1627-
{
1628-
includeDocumentMetadataChanges: false,
1629-
includeQueryMetadataChanges: true,
1630-
waitForSyncWhenOnline: true
1631-
},
1632-
{
1633-
next: (result: firestore.QuerySnapshot) => {
1634-
// Remove query first before passing event to user to avoid
1635-
// user actions affecting the now stale query.
1636-
unlisten();
1637-
1638-
resolve(result);
1639-
},
1640-
error: reject
1674+
if (options && options.source === 'cache') {
1675+
this.firestore
1676+
.ensureClientConfigured()
1677+
.getDocumentsFromLocalCache(this._query)
1678+
.then((viewSnap: ViewSnapshot) => {
1679+
resolve(new QuerySnapshot(this.firestore, this._query, viewSnap));
1680+
}, reject);
1681+
} else {
1682+
this.getViaSnapshotListener(resolve, reject, options);
1683+
}
1684+
}
1685+
);
1686+
}
1687+
1688+
private getViaSnapshotListener(
1689+
resolve: Resolver<firestore.QuerySnapshot>,
1690+
reject: Rejecter,
1691+
options?: firestore.GetOptions
1692+
): void {
1693+
const unlisten = this.onSnapshotInternal(
1694+
{
1695+
includeDocumentMetadataChanges: false,
1696+
includeQueryMetadataChanges: true,
1697+
waitForSyncWhenOnline: true
1698+
},
1699+
{
1700+
next: (result: firestore.QuerySnapshot) => {
1701+
// Remove query first before passing event to user to avoid
1702+
// user actions affecting the now stale query.
1703+
unlisten();
1704+
1705+
if (
1706+
result.metadata.fromCache &&
1707+
options &&
1708+
options.source === 'server'
1709+
) {
1710+
reject(
1711+
new FirestoreError(
1712+
Code.UNAVAILABLE,
1713+
'Failed to get documents from server. (However, these ' +
1714+
'documents may exist in the local cache. Run again ' +
1715+
'without setting source to "server" to ' +
1716+
'retrieve the cached documents.)'
1717+
)
1718+
);
1719+
} else {
1720+
resolve(result);
16411721
}
1642-
);
1722+
},
1723+
error: reject
16431724
}
16441725
);
16451726
}

0 commit comments

Comments
 (0)