diff --git a/.changeset/brown-beers-tease.md b/.changeset/brown-beers-tease.md new file mode 100644 index 00000000000..e0e8e7adc9e --- /dev/null +++ b/.changeset/brown-beers-tease.md @@ -0,0 +1,5 @@ +--- +"@firebase/firestore": minor +--- + +Introduces a new way to config Firestore SDK Cache. diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 077e0e4426f..905629f870a 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -163,10 +163,10 @@ export class DocumentSnapshot { export { EmulatorMockTokenOptions } -// @public +// @public @deprecated export function enableIndexedDbPersistence(firestore: Firestore, persistenceSettings?: PersistenceSettings): Promise; -// @public +// @public @deprecated export function enableMultiTabIndexedDbPersistence(firestore: Firestore): Promise; // @public @@ -219,6 +219,9 @@ export class FirestoreError extends FirebaseError { // @public export type FirestoreErrorCode = 'cancelled' | 'unknown' | 'invalid-argument' | 'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' | 'data-loss' | 'unauthenticated'; +// @public +export type FirestoreLocalCache = MemoryLocalCache | PersistentLocalCache; + // @public export interface FirestoreSettings { cacheSizeBytes?: number; @@ -226,6 +229,7 @@ export interface FirestoreSettings { experimentalForceLongPolling?: boolean; host?: string; ignoreUndefinedProperties?: boolean; + localCache?: FirestoreLocalCache; ssl?: boolean; } @@ -327,6 +331,15 @@ export interface LoadBundleTaskProgress { export { LogLevel } +// @public +export interface MemoryLocalCache { + // (undocumented) + kind: 'memory'; +} + +// @public +export function memoryLocalCache(): MemoryLocalCache; + // @public export function namedQuery(firestore: Firestore, name: string): Promise; @@ -404,6 +417,47 @@ export interface PersistenceSettings { forceOwnership?: boolean; } +// @public +export interface PersistentCacheSettings { + cacheSizeBytes?: number; + tabManager?: PersistentTabManager; +} + +// @public +export interface PersistentLocalCache { + // (undocumented) + kind: 'persistent'; +} + +// @public +export function persistentLocalCache(settings?: PersistentCacheSettings): PersistentLocalCache; + +// @public +export interface PersistentMultipleTabManager { + // (undocumented) + kind: 'PersistentMultipleTab'; +} + +// @public +export function persistentMultipleTabManager(): PersistentMultipleTabManager; + +// @public +export interface PersistentSingleTabManager { + // (undocumented) + kind: 'persistentSingleTab'; +} + +// @public +export function persistentSingleTabManager(settings: PersistentSingleTabManagerSettings | undefined): PersistentSingleTabManager; + +// @public +export interface PersistentSingleTabManagerSettings { + forceOwnership?: boolean; +} + +// @public +export type PersistentTabManager = PersistentSingleTabManager | PersistentMultipleTabManager; + // @public export type Primitive = string | number | boolean | undefined | null; diff --git a/docs-devsite/firestore_.firestoresettings.md b/docs-devsite/firestore_.firestoresettings.md index 96fe454cb77..8e6da365f46 100644 --- a/docs-devsite/firestore_.firestoresettings.md +++ b/docs-devsite/firestore_.firestoresettings.md @@ -22,15 +22,18 @@ export declare interface FirestoreSettings | Property | Type | Description | | --- | --- | --- | -| [cacheSizeBytes](./firestore_.firestoresettings.md#firestoresettingscachesizebytes) | number | An approximate cache size threshold for the on-disk data. If the cache grows beyond this size, Firestore will start removing data that hasn't been recently used. The size is not a guarantee that the cache will stay below that size, only that if the cache exceeds the given size, cleanup will be attempted.The default value is 40 MB. The threshold must be set to at least 1 MB, and can be set to CACHE_SIZE_UNLIMITED to disable garbage collection. | +| [cacheSizeBytes](./firestore_.firestoresettings.md#firestoresettingscachesizebytes) | number | NOTE: This field will be deprecated in a future major release. Use cache field instead to specify cache size, and other cache configurations.An approximate cache size threshold for the on-disk data. If the cache grows beyond this size, Firestore will start removing data that hasn't been recently used. The size is not a guarantee that the cache will stay below that size, only that if the cache exceeds the given size, cleanup will be attempted.The default value is 40 MB. The threshold must be set to at least 1 MB, and can be set to CACHE_SIZE_UNLIMITED to disable garbage collection. | | [experimentalAutoDetectLongPolling](./firestore_.firestoresettings.md#firestoresettingsexperimentalautodetectlongpolling) | boolean | Configures the SDK's underlying transport (WebChannel) to automatically detect if long-polling should be used. This is very similar to experimentalForceLongPolling, but only uses long-polling if required.This setting will likely be enabled by default in future releases and cannot be combined with experimentalForceLongPolling. | | [experimentalForceLongPolling](./firestore_.firestoresettings.md#firestoresettingsexperimentalforcelongpolling) | boolean | Forces the SDK’s underlying network transport (WebChannel) to use long-polling. Each response from the backend will be closed immediately after the backend sends data (by default responses are kept open in case the backend has more data to send). This avoids incompatibility issues with certain proxies, antivirus software, etc. that incorrectly buffer traffic indefinitely. Use of this option will cause some performance degradation though.This setting cannot be used with experimentalAutoDetectLongPolling and may be removed in a future release. If you find yourself using it to work around a specific network reliability issue, please tell us about it in https://github.com/firebase/firebase-js-sdk/issues/1674. | | [host](./firestore_.firestoresettings.md#firestoresettingshost) | string | The hostname to connect to. | | [ignoreUndefinedProperties](./firestore_.firestoresettings.md#firestoresettingsignoreundefinedproperties) | boolean | Whether to skip nested properties that are set to undefined during object serialization. If set to true, these properties are skipped and not written to Firestore. If set to false or omitted, the SDK throws an exception when it encounters properties of type undefined. | +| [localCache](./firestore_.firestoresettings.md#firestoresettingslocalcache) | [FirestoreLocalCache](./firestore_.md#firestorelocalcache) | Specifies the cache used by the SDK. Availabe options are MemoryLocalCache and IndexedDbLocalCache, each with different configuration options.When unspecified, MemoryLocalCache will be used by default.NOTE: setting this field and cacheSizeBytes at the same time will throw exception during SDK initialization. Instead, using the configuration in the FirestoreLocalCache object to specify the cache size. | | [ssl](./firestore_.firestoresettings.md#firestoresettingsssl) | boolean | Whether to use SSL when connecting. | ## FirestoreSettings.cacheSizeBytes +NOTE: This field will be deprecated in a future major release. Use `cache` field instead to specify cache size, and other cache configurations. + An approximate cache size threshold for the on-disk data. If the cache grows beyond this size, Firestore will start removing data that hasn't been recently used. The size is not a guarantee that the cache will stay below that size, only that if the cache exceeds the given size, cleanup will be attempted. The default value is 40 MB. The threshold must be set to at least 1 MB, and can be set to `CACHE_SIZE_UNLIMITED` to disable garbage collection. @@ -85,6 +88,20 @@ Whether to skip nested properties that are set to `undefined` during object seri ignoreUndefinedProperties?: boolean; ``` +## FirestoreSettings.localCache + +Specifies the cache used by the SDK. Availabe options are `MemoryLocalCache` and `IndexedDbLocalCache`, each with different configuration options. + +When unspecified, `MemoryLocalCache` will be used by default. + +NOTE: setting this field and `cacheSizeBytes` at the same time will throw exception during SDK initialization. Instead, using the configuration in the `FirestoreLocalCache` object to specify the cache size. + +Signature: + +```typescript +localCache?: FirestoreLocalCache; +``` + ## FirestoreSettings.ssl Whether to use SSL when connecting. diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 9351f162119..365635c434d 100644 --- a/docs-devsite/firestore_.md +++ b/docs-devsite/firestore_.md @@ -33,7 +33,7 @@ https://github.com/firebase/firebase-js-sdk | [onSnapshotsInSync(firestore, observer)](./firestore_.md#onsnapshotsinsync) | Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if a single server-generated change affects multiple listeners.NOTE: The snapshots-in-sync event only indicates that listeners are in sync with each other, but does not relate to whether those snapshots are in sync with the server. Use SnapshotMetadata in the individual listeners to determine if a snapshot is from the cache or the server. | | [onSnapshotsInSync(firestore, onSync)](./firestore_.md#onsnapshotsinsync) | Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if a single server-generated change affects multiple listeners.NOTE: The snapshots-in-sync event only indicates that listeners are in sync with each other, but does not relate to whether those snapshots are in sync with the server. Use SnapshotMetadata in the individual listeners to determine if a snapshot is from the cache or the server. | | [runTransaction(firestore, updateFunction, options)](./firestore_.md#runtransaction) | Executes the given updateFunction and then attempts to commit the changes applied within the transaction. If any document read within the transaction has changed, Cloud Firestore retries the updateFunction. If it fails to commit after 5 attempts, the transaction fails.The maximum number of writes allowed in a single transaction is 500. | -| [setIndexConfiguration(firestore, configuration)](./firestore_.md#setindexconfiguration) | (BETA) Configures indexing for local query execution. Any previous index configuration is overridden. The Promise resolves once the index configuration has been persisted.The index entries themselves are created asynchronously. You can continue to use queries that require indexing even if the indices are not yet available. Query execution will automatically start using the index once the index entries have been written.Indexes are only supported with IndexedDb persistence. Invoke either enableIndexedDbPersistence() or enableMultiTabIndexedDbPersistence() before setting an index configuration. If IndexedDb is not enabled, any index configuration is ignored. | +| [setIndexConfiguration(firestore, configuration)](./firestore_.md#setindexconfiguration) | (BETA) Configures indexing for local query execution. Any previous index configuration is overridden. The Promise resolves once the index configuration has been persisted.The index entries themselves are created asynchronously. You can continue to use queries that require indexing even if the indices are not yet available. Query execution will automatically start using the index once the index entries have been written.Indexes are only supported with IndexedDb persistence. If IndexedDb is not enabled, any index configuration is ignored. | | [setIndexConfiguration(firestore, json)](./firestore_.md#setindexconfiguration) | (BETA) Configures indexing for local query execution. Any previous index configuration is overridden. The Promise resolves once the index configuration has been persisted.The index entries themselves are created asynchronously. You can continue to use queries that require indexing even if the indices are not yet available. Query execution will automatically start using the index once the index entries have been written.Indexes are only supported with IndexedDb persistence. Invoke either enableIndexedDbPersistence() or enableMultiTabIndexedDbPersistence() before setting an index configuration. If IndexedDb is not enabled, any index configuration is ignored.The method accepts the JSON format exported by the Firebase CLI (firebase firestore:indexes). If the JSON format is invalid, this method throws an error. | | [terminate(firestore)](./firestore_.md#terminate) | Terminates the provided [Firestore](./firestore_.firestore.md#firestore_class) instance.After calling terminate() only the clearIndexedDbPersistence() function may be used. Any other function will throw a FirestoreError.To restart after termination, create a new instance of FirebaseFirestore with [getFirestore()](./firestore_.md#getfirestore).Termination does not cancel any pending writes, and any promises that are awaiting a response from the server will not be resolved. If you have persistence enabled, the next time you start this instance, it will resume sending these writes to the server.Note: Under normal circumstances, calling terminate() is not required. This function is useful only when you want to force this instance to release all of its resources or in combination with clearIndexedDbPersistence() to ensure that all local state is destroyed between test runs. | | [waitForPendingWrites(firestore)](./firestore_.md#waitforpendingwrites) | Waits until all currently pending writes for the active user have been acknowledged by the backend.The returned promise resolves immediately if there are no outstanding writes. Otherwise, the promise waits for all previously issued writes (including those written in a previous app session), but it does not wait for writes that were added after the function is called. If you want to wait for additional writes, call waitForPendingWrites() again.Any outstanding waitForPendingWrites() promises are rejected during user changes. | @@ -42,6 +42,8 @@ https://github.com/firebase/firebase-js-sdk | [deleteField()](./firestore_.md#deletefield) | Returns a sentinel for use with [updateDoc()](./firestore_lite.md#updatedoc) or [setDoc()](./firestore_lite.md#setdoc) with {merge: true} to mark a field for deletion. | | [documentId()](./firestore_.md#documentid) | Returns a special sentinel FieldPath to refer to the ID of a document. It can be used in queries to sort or filter by the document ID. | | [getFirestore()](./firestore_.md#getfirestore) | Returns the existing default [Firestore](./firestore_.firestore.md#firestore_class) instance that is associated with the default [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface). If no instance exists, initializes a new instance with default settings. | +| [memoryLocalCache()](./firestore_.md#memorylocalcache) | Creates an instance of MemoryLocalCache. The instance can be set to FirestoreSettings.cache to tell the SDK which cache layer to use. | +| [persistentMultipleTabManager()](./firestore_.md#persistentmultipletabmanager) | Creates an instance of PersistentMultipleTabManager. | | [serverTimestamp()](./firestore_.md#servertimestamp) | Returns a sentinel used with [setDoc()](./firestore_lite.md#setdoc) or [updateDoc()](./firestore_lite.md#updatedoc) to include a server-generated timestamp in the written data. | | function(elements...) | | [arrayRemove(elements)](./firestore_.md#arrayremove) | Returns a special value that can be used with [setDoc()](./firestore_.md#setdoc) or that tells the server to remove the given elements from any array value that already exists on the server. All instances of each element specified will be removed from the array. If the field being modified is not already an array it will be overwritten with an empty array. | @@ -98,6 +100,9 @@ https://github.com/firebase/firebase-js-sdk | [setDoc(reference, data, options)](./firestore_.md#setdoc) | Writes to the document referred to by the specified DocumentReference. If the document does not yet exist, it will be created. If you provide merge or mergeFields, the provided data can be merged into an existing document. | | [updateDoc(reference, data)](./firestore_.md#updatedoc) | Updates fields in the document referred to by the specified DocumentReference. The update will fail if applied to a document that does not exist. | | [updateDoc(reference, field, value, moreFieldsAndValues)](./firestore_.md#updatedoc) | Updates fields in the document referred to by the specified DocumentReference The update will fail if applied to a document that does not exist.Nested fields can be updated by providing dot-separated field path strings or by providing FieldPath objects. | +| function(settings...) | +| [persistentLocalCache(settings)](./firestore_.md#persistentlocalcache) | Creates an instance of PersistentLocalCache. The instance can be set to FirestoreSettings.cache to tell the SDK which cache layer to use. | +| [persistentSingleTabManager(settings)](./firestore_.md#persistentsingletabmanager) | Creates an instance of PersistentSingleTabManager. | | function(snapshot...) | | [endAt(snapshot)](./firestore_.md#endat) | Creates a [QueryEndAtConstraint](./firestore_.queryendatconstraint.md#queryendatconstraint_class) that modifies the result set to end at the provided document (inclusive). The end position is relative to the order of the query. The document must contain all of the fields provided in the orderBy of the query. | | [endBefore(snapshot)](./firestore_.md#endbefore) | Creates a [QueryEndAtConstraint](./firestore_.queryendatconstraint.md#queryendatconstraint_class) that modifies the result set to end before the provided document (exclusive). The end position is relative to the order of the query. The document must contain all of the fields provided in the orderBy of the query. | @@ -148,7 +153,13 @@ https://github.com/firebase/firebase-js-sdk | [IndexConfiguration](./firestore_.indexconfiguration.md#indexconfiguration_interface) | (BETA) A list of Firestore indexes to speed up local query execution.See [JSON Format](https://firebase.google.com/docs/reference/firestore/indexes/#json_format) for a description of the format of the index definition. | | [IndexField](./firestore_.indexfield.md#indexfield_interface) | (BETA) A single field element in an index configuration. | | [LoadBundleTaskProgress](./firestore_.loadbundletaskprogress.md#loadbundletaskprogress_interface) | Represents a progress update or a final state from loading bundles. | +| [MemoryLocalCache](./firestore_.memorylocalcache.md#memorylocalcache_interface) | Provides an in-memory cache to the SDK. This is the default cache unless explicitly configured otherwise.To use, create an instance using the factory function , then set the instance to FirestoreSettings.cache and call initializeFirestore using the settings object. | | [PersistenceSettings](./firestore_.persistencesettings.md#persistencesettings_interface) | Settings that can be passed to enableIndexedDbPersistence() to configure Firestore persistence. | +| [PersistentCacheSettings](./firestore_.persistentcachesettings.md#persistentcachesettings_interface) | An settings object to configure an PersistentLocalCache instance. | +| [PersistentLocalCache](./firestore_.persistentlocalcache.md#persistentlocalcache_interface) | Provides a persistent cache backed by IndexedDb to the SDK.To use, create an instance using the factory function , then set the instance to FirestoreSettings.cache and call initializeFirestore using the settings object. | +| [PersistentMultipleTabManager](./firestore_.persistentmultipletabmanager.md#persistentmultipletabmanager_interface) | A tab manager supportting multiple tabs. SDK will synchronize queries and mutations done across all tabs using the SDK. | +| [PersistentSingleTabManager](./firestore_.persistentsingletabmanager.md#persistentsingletabmanager_interface) | A tab manager supportting only one tab, no synchronization will be performed across tabs. | +| [PersistentSingleTabManagerSettings](./firestore_.persistentsingletabmanagersettings.md#persistentsingletabmanagersettings_interface) | Type to configure an PersistentSingleTabManager instace. | | [SnapshotListenOptions](./firestore_.snapshotlistenoptions.md#snapshotlistenoptions_interface) | An options object that can be passed to [onSnapshot()](./firestore_.md#onsnapshot) and [QuerySnapshot.docChanges()](./firestore_.querysnapshot.md#querysnapshotdocchanges) to control which types of changes to include in the result set. | | [SnapshotOptions](./firestore_.snapshotoptions.md#snapshotoptions_interface) | Options that configure how data is retrieved from a DocumentSnapshot (for example the desired behavior for server timestamps that have not yet been set to their final value). | | [TransactionOptions](./firestore_.transactionoptions.md#transactionoptions_interface) | Options to customize transaction behavior. | @@ -170,9 +181,11 @@ https://github.com/firebase/firebase-js-sdk | [ChildUpdateFields](./firestore_.md#childupdatefields) | Helper for calculating the nested fields for a given type T1. This is needed to distribute union types such as undefined | {...} (happens for optional props) or {a: A} | {b: B}.In this use case, V is used to distribute the union types of T[K] on Record, since T[K] is evaluated as an expression and not distributed.See https://www.typescriptlang.org/docs/handbook/advanced-types.html\#distributive-conditional-types | | [DocumentChangeType](./firestore_.md#documentchangetype) | The type of a DocumentChange may be 'added', 'removed', or 'modified'. | | [FirestoreErrorCode](./firestore_.md#firestoreerrorcode) | The set of Firestore status codes. The codes are the same at the ones exposed by gRPC here: https://github.com/grpc/grpc/blob/master/doc/statuscodes.mdPossible values: - 'cancelled': The operation was cancelled (typically by the caller). - 'unknown': Unknown error or an error from a different error domain. - 'invalid-argument': Client specified an invalid argument. Note that this differs from 'failed-precondition'. 'invalid-argument' indicates arguments that are problematic regardless of the state of the system (e.g. an invalid field name). - 'deadline-exceeded': Deadline expired before operation could complete. For operations that change the state of the system, this error may be returned even if the operation has completed successfully. For example, a successful response from a server could have been delayed long enough for the deadline to expire. - 'not-found': Some requested document was not found. - 'already-exists': Some document that we attempted to create already exists. - 'permission-denied': The caller does not have permission to execute the specified operation. - 'resource-exhausted': Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. - 'failed-precondition': Operation was rejected because the system is not in a state required for the operation's execution. - 'aborted': The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. - 'out-of-range': Operation was attempted past the valid range. - 'unimplemented': Operation is not implemented or not supported/enabled. - 'internal': Internal errors. Means some invariants expected by underlying system has been broken. If you see one of these errors, something is very broken. - 'unavailable': The service is currently unavailable. This is most likely a transient condition and may be corrected by retrying with a backoff. - 'data-loss': Unrecoverable data loss or corruption. - 'unauthenticated': The request does not have valid authentication credentials for the operation. | +| [FirestoreLocalCache](./firestore_.md#firestorelocalcache) | Union type from all supported SDK cache layer. | | [NestedUpdateFields](./firestore_.md#nestedupdatefields) | For each field (e.g. 'bar'), find all nested keys (e.g. {'bar.baz': T1, 'bar.qux': T2}). Intersect them together to make a single map containing all possible keys that are all marked as optional | | [OrderByDirection](./firestore_.md#orderbydirection) | The direction of a [orderBy()](./firestore_.md#orderby) clause is specified as 'desc' or 'asc' (descending or ascending). | | [PartialWithFieldValue](./firestore_.md#partialwithfieldvalue) | Similar to Typescript's Partial<T>, but allows nested fields to be omitted and FieldValues to be passed in as property values. | +| [PersistentTabManager](./firestore_.md#persistenttabmanager) | A union of all avaialbe tab managers. | | [Primitive](./firestore_.md#primitive) | Primitive types. | | [QueryConstraintType](./firestore_.md#queryconstrainttype) | Describes the different query constraints available in this SDK. | | [QueryFilterConstraint](./firestore_.md#queryfilterconstraint) | QueryFilterConstraint is a helper union type that represents [QueryFieldFilterConstraint](./firestore_.queryfieldfilterconstraint.md#queryfieldfilterconstraint_class) and [QueryCompositeFilterConstraint](./firestore_.querycompositefilterconstraint.md#querycompositefilterconstraint_class). | @@ -386,6 +399,11 @@ If the final path has an odd number of segments and does not point to a document ## enableIndexedDbPersistence() +> Warning: This API is now obsolete. +> +> This function will be removed in a future major release. Instead, set `FirestoreSettings.cache` to an instance of `IndexedDbLocalCache` to turn on IndexedDb cache. Calling this function when `FirestoreSettings.cache` is already specified will throw an exception. +> + Attempts to enable persistent storage, if possible. Must be called before any other functions (other than [initializeFirestore()](./firestore_.md#initializefirestore), [getFirestore()](./firestore_.md#getfirestore) or [clearIndexedDbPersistence()](./firestore_.md#clearindexeddbpersistence). @@ -417,6 +435,11 @@ A `Promise` that represents successfully enabling persistent storage. ## enableMultiTabIndexedDbPersistence() +> Warning: This API is now obsolete. +> +> This function will be removed in a future major release. Instead, set `FirestoreSettings.cache` to an instance of `IndexedDbLocalCache` to turn on indexeddb cache. Calling this function when `FirestoreSettings.cache` is already specified will throw an exception. +> + Attempts to enable multi-tab persistent storage, if possible. If enabled across all tabs, all operations share access to local persistence, including shared execution of queries and latency-compensated local document updates across all connected instances. If this fails, `enableMultiTabIndexedDbPersistence()` will reject the promise it returns. Note that even after this failure, the [Firestore](./firestore_.firestore.md#firestore_class) instance will remain usable, however offline persistence will be disabled. @@ -602,7 +625,7 @@ Configures indexing for local query execution. Any previous index configuration The index entries themselves are created asynchronously. You can continue to use queries that require indexing even if the indices are not yet available. Query execution will automatically start using the index once the index entries have been written. -Indexes are only supported with IndexedDb persistence. Invoke either `enableIndexedDbPersistence()` or `enableMultiTabIndexedDbPersistence()` before setting an index configuration. If IndexedDb is not enabled, any index configuration is ignored. +Indexes are only supported with IndexedDb persistence. If IndexedDb is not enabled, any index configuration is ignored. Signature: @@ -784,6 +807,32 @@ export declare function getFirestore(): Firestore; The [Firestore](./firestore_.firestore.md#firestore_class) instance of the provided app. +## memoryLocalCache() + +Creates an instance of `MemoryLocalCache`. The instance can be set to `FirestoreSettings.cache` to tell the SDK which cache layer to use. + +Signature: + +```typescript +export declare function memoryLocalCache(): MemoryLocalCache; +``` +Returns: + +[MemoryLocalCache](./firestore_.memorylocalcache.md#memorylocalcache_interface) + +## persistentMultipleTabManager() + +Creates an instance of `PersistentMultipleTabManager`. + +Signature: + +```typescript +export declare function persistentMultipleTabManager(): PersistentMultipleTabManager; +``` +Returns: + +[PersistentMultipleTabManager](./firestore_.persistentmultipletabmanager.md#persistentmultipletabmanager_interface) + ## serverTimestamp() Returns a sentinel used with [setDoc()](./firestore_lite.md#setdoc) or [updateDoc()](./firestore_lite.md#updatedoc) to include a server-generated timestamp in the written data. @@ -1905,6 +1954,46 @@ Promise<void> A `Promise` resolved once the data has been successfully written to the backend (note that it won't resolve while you're offline). +## persistentLocalCache() + +Creates an instance of `PersistentLocalCache`. The instance can be set to `FirestoreSettings.cache` to tell the SDK which cache layer to use. + +Signature: + +```typescript +export declare function persistentLocalCache(settings?: PersistentCacheSettings): PersistentLocalCache; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| settings | [PersistentCacheSettings](./firestore_.persistentcachesettings.md#persistentcachesettings_interface) | | + +Returns: + +[PersistentLocalCache](./firestore_.persistentlocalcache.md#persistentlocalcache_interface) + +## persistentSingleTabManager() + +Creates an instance of `PersistentSingleTabManager`. + +Signature: + +```typescript +export declare function persistentSingleTabManager(settings: PersistentSingleTabManagerSettings | undefined): PersistentSingleTabManager; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| settings | [PersistentSingleTabManagerSettings](./firestore_.persistentsingletabmanagersettings.md#persistentsingletabmanagersettings_interface) \| undefined | Configures the created tab manager. | + +Returns: + +[PersistentSingleTabManager](./firestore_.persistentsingletabmanager.md#persistentsingletabmanager_interface) + ## endAt() Creates a [QueryEndAtConstraint](./firestore_.queryendatconstraint.md#queryendatconstraint_class) that modifies the result set to end at the provided document (inclusive). The end position is relative to the order of the query. The document must contain all of the fields provided in the orderBy of the query. @@ -2073,6 +2162,16 @@ Possible values: - 'cancelled': The operation was cancelled (typically by the ca export declare type FirestoreErrorCode = 'cancelled' | 'unknown' | 'invalid-argument' | 'deadline-exceeded' | 'not-found' | 'already-exists' | 'permission-denied' | 'resource-exhausted' | 'failed-precondition' | 'aborted' | 'out-of-range' | 'unimplemented' | 'internal' | 'unavailable' | 'data-loss' | 'unauthenticated'; ``` +## FirestoreLocalCache + +Union type from all supported SDK cache layer. + +Signature: + +```typescript +export declare type FirestoreLocalCache = MemoryLocalCache | PersistentLocalCache; +``` + ## NestedUpdateFields For each field (e.g. 'bar'), find all nested keys (e.g. {'bar.baz': T1, 'bar.qux': T2}). Intersect them together to make a single map containing all possible keys that are all marked as optional @@ -2107,6 +2206,16 @@ export declare type PartialWithFieldValue = Partial | (T extends Primitive } : never); ``` +## PersistentTabManager + +A union of all avaialbe tab managers. + +Signature: + +```typescript +export declare type PersistentTabManager = PersistentSingleTabManager | PersistentMultipleTabManager; +``` + ## Primitive Primitive types. diff --git a/docs-devsite/firestore_.memorylocalcache.md b/docs-devsite/firestore_.memorylocalcache.md new file mode 100644 index 00000000000..92b7d3a2c72 --- /dev/null +++ b/docs-devsite/firestore_.memorylocalcache.md @@ -0,0 +1,35 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# MemoryLocalCache interface +Provides an in-memory cache to the SDK. This is the default cache unless explicitly configured otherwise. + +To use, create an instance using the factory function , then set the instance to `FirestoreSettings.cache` and call `initializeFirestore` using the settings object. + +Signature: + +```typescript +export declare interface MemoryLocalCache +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [kind](./firestore_.memorylocalcache.md#memorylocalcachekind) | 'memory' | | + +## MemoryLocalCache.kind + +Signature: + +```typescript +kind: 'memory'; +``` diff --git a/docs-devsite/firestore_.persistentcachesettings.md b/docs-devsite/firestore_.persistentcachesettings.md new file mode 100644 index 00000000000..dd491918894 --- /dev/null +++ b/docs-devsite/firestore_.persistentcachesettings.md @@ -0,0 +1,48 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# PersistentCacheSettings interface +An settings object to configure an `PersistentLocalCache` instance. + +Signature: + +```typescript +export declare interface PersistentCacheSettings +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [cacheSizeBytes](./firestore_.persistentcachesettings.md#persistentcachesettingscachesizebytes) | number | An approximate cache size threshold for the on-disk data. If the cache grows beyond this size, Firestore will start removing data that hasn't been recently used. The SDK does not guarantee that the cache will stay below that size, only that if the cache exceeds the given size, cleanup will be attempted.The default value is 40 MB. The threshold must be set to at least 1 MB, and can be set to CACHE_SIZE_UNLIMITED to disable garbage collection. | +| [tabManager](./firestore_.persistentcachesettings.md#persistentcachesettingstabmanager) | [PersistentTabManager](./firestore_.md#persistenttabmanager) | Specifies how multiple tabs/windows will be managed by the SDK. | + +## PersistentCacheSettings.cacheSizeBytes + +An approximate cache size threshold for the on-disk data. If the cache grows beyond this size, Firestore will start removing data that hasn't been recently used. The SDK does not guarantee that the cache will stay below that size, only that if the cache exceeds the given size, cleanup will be attempted. + +The default value is 40 MB. The threshold must be set to at least 1 MB, and can be set to `CACHE_SIZE_UNLIMITED` to disable garbage collection. + +Signature: + +```typescript +cacheSizeBytes?: number; +``` + +## PersistentCacheSettings.tabManager + +Specifies how multiple tabs/windows will be managed by the SDK. + +Signature: + +```typescript +tabManager?: PersistentTabManager; +``` diff --git a/docs-devsite/firestore_.persistentlocalcache.md b/docs-devsite/firestore_.persistentlocalcache.md new file mode 100644 index 00000000000..48d876d15bd --- /dev/null +++ b/docs-devsite/firestore_.persistentlocalcache.md @@ -0,0 +1,35 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# PersistentLocalCache interface +Provides a persistent cache backed by IndexedDb to the SDK. + +To use, create an instance using the factory function , then set the instance to `FirestoreSettings.cache` and call `initializeFirestore` using the settings object. + +Signature: + +```typescript +export declare interface PersistentLocalCache +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [kind](./firestore_.persistentlocalcache.md#persistentlocalcachekind) | 'persistent' | | + +## PersistentLocalCache.kind + +Signature: + +```typescript +kind: 'persistent'; +``` diff --git a/docs-devsite/firestore_.persistentmultipletabmanager.md b/docs-devsite/firestore_.persistentmultipletabmanager.md new file mode 100644 index 00000000000..b0c21b378a1 --- /dev/null +++ b/docs-devsite/firestore_.persistentmultipletabmanager.md @@ -0,0 +1,33 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# PersistentMultipleTabManager interface +A tab manager supportting multiple tabs. SDK will synchronize queries and mutations done across all tabs using the SDK. + +Signature: + +```typescript +export declare interface PersistentMultipleTabManager +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [kind](./firestore_.persistentmultipletabmanager.md#persistentmultipletabmanagerkind) | 'PersistentMultipleTab' | | + +## PersistentMultipleTabManager.kind + +Signature: + +```typescript +kind: 'PersistentMultipleTab'; +``` diff --git a/docs-devsite/firestore_.persistentsingletabmanager.md b/docs-devsite/firestore_.persistentsingletabmanager.md new file mode 100644 index 00000000000..ee130b6fc6a --- /dev/null +++ b/docs-devsite/firestore_.persistentsingletabmanager.md @@ -0,0 +1,33 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# PersistentSingleTabManager interface +A tab manager supportting only one tab, no synchronization will be performed across tabs. + +Signature: + +```typescript +export declare interface PersistentSingleTabManager +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [kind](./firestore_.persistentsingletabmanager.md#persistentsingletabmanagerkind) | 'persistentSingleTab' | | + +## PersistentSingleTabManager.kind + +Signature: + +```typescript +kind: 'persistentSingleTab'; +``` diff --git a/docs-devsite/firestore_.persistentsingletabmanagersettings.md b/docs-devsite/firestore_.persistentsingletabmanagersettings.md new file mode 100644 index 00000000000..de5ddc71b5c --- /dev/null +++ b/docs-devsite/firestore_.persistentsingletabmanagersettings.md @@ -0,0 +1,35 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# PersistentSingleTabManagerSettings interface +Type to configure an `PersistentSingleTabManager` instace. + +Signature: + +```typescript +export declare interface PersistentSingleTabManagerSettings +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [forceOwnership](./firestore_.persistentsingletabmanagersettings.md#persistentsingletabmanagersettingsforceownership) | boolean | Whether to force-enable persistent (IndexedDB) cache for the client. This cannot be used with multi-tab synchronization and is primarily intended for use with Web Workers. Setting this to true will enable IndexedDB, but cause other tabs using IndexedDB cache to fail. | + +## PersistentSingleTabManagerSettings.forceOwnership + +Whether to force-enable persistent (IndexedDB) cache for the client. This cannot be used with multi-tab synchronization and is primarily intended for use with Web Workers. Setting this to `true` will enable IndexedDB, but cause other tabs using IndexedDB cache to fail. + +Signature: + +```typescript +forceOwnership?: boolean; +``` diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index 93da0b1ee08..807e5dcd647 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -34,6 +34,21 @@ export { AggregateType } from './lite-api/aggregate_types'; +export { + FirestoreLocalCache, + PersistentLocalCache, + PersistentMultipleTabManager, + persistentLocalCache, + persistentMultipleTabManager, + PersistentCacheSettings, + persistentSingleTabManager, + PersistentSingleTabManager, + PersistentSingleTabManagerSettings, + MemoryLocalCache, + memoryLocalCache, + PersistentTabManager +} from './api/cache_config'; + export { FieldPath, documentId } from './api/field_path'; export { diff --git a/packages/firestore/src/api/cache_config.ts b/packages/firestore/src/api/cache_config.ts new file mode 100644 index 00000000000..a6e05bc6d7f --- /dev/null +++ b/packages/firestore/src/api/cache_config.ts @@ -0,0 +1,302 @@ +/** + * @license + * Copyright 2023 Google LLC + * + * 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 { + IndexedDbOfflineComponentProvider, + MemoryOfflineComponentProvider, + MultiTabOfflineComponentProvider, + OfflineComponentProvider, + OnlineComponentProvider +} from '../core/component_provider'; + +/* eslint @typescript-eslint/consistent-type-definitions: ["error", "type"] */ +/** + * Provides an in-memory cache to the SDK. This is the default cache unless explicitly + * configured otherwise. + * + * To use, create an instance using the factory function {@link memoryLocalCache()}, then + * set the instance to `FirestoreSettings.cache` and call `initializeFirestore` using + * the settings object. + */ +export type MemoryLocalCache = { + kind: 'memory'; + /** + * @internal + */ + _onlineComponentProvider: OnlineComponentProvider; + /** + * @internal + */ + _offlineComponentProvider: MemoryOfflineComponentProvider; +}; + +class MemoryLocalCacheImpl implements MemoryLocalCache { + kind: 'memory' = 'memory'; + /** + * @internal + */ + _onlineComponentProvider: OnlineComponentProvider; + /** + * @internal + */ + _offlineComponentProvider: MemoryOfflineComponentProvider; + + constructor() { + this._onlineComponentProvider = new OnlineComponentProvider(); + this._offlineComponentProvider = new MemoryOfflineComponentProvider(); + } + + toJSON(): {} { + return { kind: this.kind }; + } +} + +/** + * Provides a persistent cache backed by IndexedDb to the SDK. + * + * To use, create an instance using the factory function {@link persistentLocalCache()}, then + * set the instance to `FirestoreSettings.cache` and call `initializeFirestore` using + * the settings object. + */ +export type PersistentLocalCache = { + kind: 'persistent'; + /** + * @internal + */ + _onlineComponentProvider: OnlineComponentProvider; + /** + * @internal + */ + _offlineComponentProvider: OfflineComponentProvider; +}; + +class PersistentLocalCacheImpl implements PersistentLocalCache { + kind: 'persistent' = 'persistent'; + /** + * @internal + */ + _onlineComponentProvider: OnlineComponentProvider; + /** + * @internal + */ + _offlineComponentProvider: OfflineComponentProvider; + + constructor(settings: PersistentCacheSettings | undefined) { + let tabManager: PersistentTabManager; + if (settings?.tabManager) { + settings.tabManager._initialize(settings); + tabManager = settings.tabManager; + } else { + tabManager = persistentSingleTabManager(undefined); + tabManager._initialize(settings); + } + this._onlineComponentProvider = tabManager._onlineComponentProvider!; + this._offlineComponentProvider = tabManager._offlineComponentProvider!; + } + + toJSON(): {} { + return { kind: this.kind }; + } +} + +/** + * Union type from all supported SDK cache layer. + */ +export type FirestoreLocalCache = MemoryLocalCache | PersistentLocalCache; + +/** + * Creates an instance of `MemoryLocalCache`. The instance can be set to + * `FirestoreSettings.cache` to tell the SDK which cache layer to use. + */ +export function memoryLocalCache(): MemoryLocalCache { + return new MemoryLocalCacheImpl(); +} + +/** + * An settings object to configure an `PersistentLocalCache` instance. + */ +export type PersistentCacheSettings = { + /** + * An approximate cache size threshold for the on-disk data. If the cache + * grows beyond this size, Firestore will start removing data that hasn't been + * recently used. The SDK does not guarantee that the cache will stay below + * that size, only that if the cache exceeds the given size, cleanup will be + * attempted. + * + * The default value is 40 MB. The threshold must be set to at least 1 MB, and + * can be set to `CACHE_SIZE_UNLIMITED` to disable garbage collection. + */ + cacheSizeBytes?: number; + + /** + * Specifies how multiple tabs/windows will be managed by the SDK. + */ + tabManager?: PersistentTabManager; +}; + +/** + * Creates an instance of `PersistentLocalCache`. The instance can be set to + * `FirestoreSettings.cache` to tell the SDK which cache layer to use. + */ +export function persistentLocalCache( + settings?: PersistentCacheSettings +): PersistentLocalCache { + return new PersistentLocalCacheImpl(settings); +} + +/** + * A tab manager supportting only one tab, no synchronization will be + * performed across tabs. + */ +export type PersistentSingleTabManager = { + kind: 'persistentSingleTab'; + /** + * @internal + */ + _initialize: ( + settings: Omit | undefined + ) => void; + /** + * @internal + */ + _onlineComponentProvider?: OnlineComponentProvider; + /** + * @internal + */ + _offlineComponentProvider?: OfflineComponentProvider; +}; + +class SingleTabManagerImpl implements PersistentSingleTabManager { + kind: 'persistentSingleTab' = 'persistentSingleTab'; + + /** + * @internal + */ + _onlineComponentProvider?: OnlineComponentProvider; + /** + * @internal + */ + _offlineComponentProvider?: OfflineComponentProvider; + + constructor(private forceOwnership?: boolean) {} + + toJSON(): {} { + return { kind: this.kind }; + } + + /** + * @internal + */ + _initialize( + settings: Omit | undefined + ): void { + this._onlineComponentProvider = new OnlineComponentProvider(); + this._offlineComponentProvider = new IndexedDbOfflineComponentProvider( + this._onlineComponentProvider, + settings?.cacheSizeBytes, + this.forceOwnership + ); + } +} + +/** + * A tab manager supportting multiple tabs. SDK will synchronize queries and + * mutations done across all tabs using the SDK. + */ +export type PersistentMultipleTabManager = { + kind: 'PersistentMultipleTab'; + /** + * @internal + */ + _initialize: (settings: Omit) => void; + /** + * @internal + */ + _onlineComponentProvider?: OnlineComponentProvider; + /** + * @internal + */ + + _offlineComponentProvider?: OfflineComponentProvider; +}; + +class MultiTabManagerImpl implements PersistentMultipleTabManager { + kind: 'PersistentMultipleTab' = 'PersistentMultipleTab'; + + /** + * @internal + */ + _onlineComponentProvider?: OnlineComponentProvider; + /** + * @internal + */ + _offlineComponentProvider?: OfflineComponentProvider; + + toJSON(): {} { + return { kind: this.kind }; + } + + /** + * @internal + */ + _initialize( + settings: Omit | undefined + ): void { + this._onlineComponentProvider = new OnlineComponentProvider(); + this._offlineComponentProvider = new MultiTabOfflineComponentProvider( + this._onlineComponentProvider, + settings?.cacheSizeBytes + ); + } +} + +/** + * A union of all avaialbe tab managers. + */ +export type PersistentTabManager = + | PersistentSingleTabManager + | PersistentMultipleTabManager; + +/** + * Type to configure an `PersistentSingleTabManager` instace. + */ +export type PersistentSingleTabManagerSettings = { + /** + * Whether to force-enable persistent (IndexedDB) cache for the client. This + * cannot be used with multi-tab synchronization and is primarily intended for + * use with Web Workers. Setting this to `true` will enable IndexedDB, but cause + * other tabs using IndexedDB cache to fail. + */ + forceOwnership?: boolean; +}; +/** + * Creates an instance of `PersistentSingleTabManager`. + * + * @param settings Configures the created tab manager. + */ +export function persistentSingleTabManager( + settings: PersistentSingleTabManagerSettings | undefined +): PersistentSingleTabManager { + return new SingleTabManagerImpl(settings?.forceOwnership); +} + +/** + * Creates an instance of `PersistentMultipleTabManager`. + */ +export function persistentMultipleTabManager(): PersistentMultipleTabManager { + return new MultiTabManagerImpl(); +} diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 29cca7e68ca..93aab4880d9 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -32,6 +32,7 @@ import { } from '../core/component_provider'; import { DatabaseId, DEFAULT_DATABASE_NAME } from '../core/database_info'; import { + canFallbackFromIndexedDbError, FirestoreClient, firestoreClientDisableNetwork, firestoreClientEnableNetwork, @@ -75,11 +76,6 @@ declare module '@firebase/component' { } } -/** DOMException error code constants. */ -const DOM_EXCEPTION_INVALID_STATE = 11; -const DOM_EXCEPTION_ABORTED = 20; -const DOM_EXCEPTION_QUOTA_EXCEEDED = 22; - /** * Constant used to indicate the LRU garbage collection should be disabled. * Set this value as the `cacheSizeBytes` on the settings passed to the @@ -171,6 +167,17 @@ export function initializeFirestore( } } + if ( + settings.cacheSizeBytes !== undefined && + settings.localCache !== undefined + ) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `cache and cacheSizeBytes cannot be specified at the same time as cacheSizeBytes will` + + `be deprecated. Instead, specify the cache size in the cache object` + ); + } + if ( settings.cacheSizeBytes !== undefined && settings.cacheSizeBytes !== CACHE_SIZE_UNLIMITED && @@ -283,6 +290,16 @@ export function configureFirestore(firestore: Firestore): void { firestore._queue, databaseInfo ); + if ( + settings.cache?._offlineComponentProvider && + settings.cache?._onlineComponentProvider + ) { + firestore._firestoreClient._uninitializedComponentsProvider = { + _offlineKind: settings.cache.kind, + _offline: settings.cache._offlineComponentProvider, + _online: settings.cache._onlineComponentProvider + }; + } } /** @@ -307,6 +324,10 @@ export function configureFirestore(firestore: Firestore): void { * @param persistenceSettings - Optional settings object to configure * persistence. * @returns A `Promise` that represents successfully enabling persistent storage. + * @deprecated This function will be removed in a future major release. Instead, set + * `FirestoreSettings.cache` to an instance of `IndexedDbLocalCache` to + * turn on IndexedDb cache. Calling this function when `FirestoreSettings.cache` + * is already specified will throw an exception. */ export function enableIndexedDbPersistence( firestore: Firestore, @@ -316,6 +337,17 @@ export function enableIndexedDbPersistence( verifyNotInitialized(firestore); const client = ensureFirestoreConfigured(firestore); + if (client._uninitializedComponentsProvider) { + throw new FirestoreError( + Code.FAILED_PRECONDITION, + 'SDK cache is already specified.' + ); + } + + logWarn( + 'enableIndexedDbPersistence() will be deprecated in the future, ' + + 'you can use `FirestoreSettings.cache` instead.' + ); const settings = firestore._freezeSettings(); const onlineComponentProvider = new OnlineComponentProvider(); @@ -352,6 +384,10 @@ export function enableIndexedDbPersistence( * @param firestore - The {@link Firestore} instance to enable persistence for. * @returns A `Promise` that represents successfully enabling persistent * storage. + * @deprecated This function will be removed in a future major release. Instead, set + * `FirestoreSettings.cache` to an instance of `IndexedDbLocalCache` to + * turn on indexeddb cache. Calling this function when `FirestoreSettings.cache` + * is already specified will throw an exception. */ export function enableMultiTabIndexedDbPersistence( firestore: Firestore @@ -360,6 +396,17 @@ export function enableMultiTabIndexedDbPersistence( verifyNotInitialized(firestore); const client = ensureFirestoreConfigured(firestore); + if (client._uninitializedComponentsProvider) { + throw new FirestoreError( + Code.FAILED_PRECONDITION, + 'SDK cache is already specified.' + ); + } + + logWarn( + 'enableMultiTabIndexedDbPersistence() will be deprecated in the future, ' + + 'you can use `FirestoreSettings.cache` instead.' + ); const settings = firestore._freezeSettings(); const onlineComponentProvider = new OnlineComponentProvider(); @@ -398,8 +445,8 @@ function setPersistenceProviders( throw error; } logWarn( - 'Error enabling offline persistence. Falling back to ' + - 'persistence disabled: ' + + 'Error enabling indexeddb cache. Falling back to ' + + 'memory cache: ' + error ); persistenceResult.reject(error); @@ -408,44 +455,6 @@ function setPersistenceProviders( .then(() => persistenceResult.promise); } -/** - * Decides whether the provided error allows us to gracefully disable - * persistence (as opposed to crashing the client). - */ -function canFallbackFromIndexedDbError( - error: FirestoreError | DOMException -): boolean { - if (error.name === 'FirebaseError') { - return ( - error.code === Code.FAILED_PRECONDITION || - error.code === Code.UNIMPLEMENTED - ); - } else if ( - typeof DOMException !== 'undefined' && - error instanceof DOMException - ) { - // There are a few known circumstances where we can open IndexedDb but - // trying to read/write will fail (e.g. quota exceeded). For - // well-understood cases, we attempt to detect these and then gracefully - // fall back to memory persistence. - // NOTE: Rather than continue to add to this list, we could decide to - // always fall back, with the risk that we might accidentally hide errors - // representing actual SDK bugs. - return ( - // When the browser is out of quota we could get either quota exceeded - // or an aborted error depending on whether the error happened during - // schema migration. - error.code === DOM_EXCEPTION_QUOTA_EXCEEDED || - error.code === DOM_EXCEPTION_ABORTED || - // Firefox Private Browsing mode disables IndexedDb and returns - // INVALID_STATE for any usage. - error.code === DOM_EXCEPTION_INVALID_STATE - ); - } - - return true; -} - /** * Clears the persistent storage. This includes pending writes and cached * documents. diff --git a/packages/firestore/src/api/index_configuration.ts b/packages/firestore/src/api/index_configuration.ts index 9ea31a28db1..94754636176 100644 --- a/packages/firestore/src/api/index_configuration.ts +++ b/packages/firestore/src/api/index_configuration.ts @@ -15,9 +15,8 @@ * limitations under the License. */ -import { getLocalStore } from '../core/firestore_client'; +import { firestoreClientSetIndexConfiguration } from '../core/firestore_client'; import { fieldPathFromDotSeparatedString } from '../lite-api/user_data_reader'; -import { localStoreConfigureFieldIndexes } from '../local/local_store_impl'; import { FieldIndex, IndexKind, @@ -97,10 +96,8 @@ export interface IndexConfiguration { * Query execution will automatically start using the index once the index * entries have been written. * - * Indexes are only supported with IndexedDb persistence. Invoke either - * `enableIndexedDbPersistence()` or `enableMultiTabIndexedDbPersistence()` - * before setting an index configuration. If IndexedDb is not enabled, any - * index configuration is ignored. + * Indexes are only supported with IndexedDb persistence. If IndexedDb is not + * enabled, any index configuration is ignored. * * @param firestore - The {@link Firestore} instance to configure indexes for. * @param configuration -The index definition. @@ -151,17 +148,17 @@ export function setIndexConfiguration( ): Promise { firestore = cast(firestore, Firestore); const client = ensureFirestoreConfigured(firestore); - - // PORTING NOTE: We don't return an error if the user has not enabled - // persistence since `enableIndexeddbPersistence()` can fail on the Web. - if (!client.offlineComponents?.indexBackfillerScheduler) { + if ( + !client._uninitializedComponentsProvider || + client._uninitializedComponentsProvider?._offlineKind === 'memory' + ) { + // PORTING NOTE: We don't return an error if the user has not enabled + // persistence since `enableIndexeddbPersistence()` can fail on the Web. logWarn('Cannot enable indexes when persistence is disabled'); return Promise.resolve(); } const parsedIndexes = parseIndexes(jsonOrConfiguration); - return getLocalStore(client).then(localStore => - localStoreConfigureFieldIndexes(localStore, parsedIndexes) - ); + return firestoreClientSetIndexConfiguration(client, parsedIndexes); } export function parseIndexes( diff --git a/packages/firestore/src/api/settings.ts b/packages/firestore/src/api/settings.ts index f6e92854495..0a9dc9d7ebe 100644 --- a/packages/firestore/src/api/settings.ts +++ b/packages/firestore/src/api/settings.ts @@ -17,6 +17,8 @@ import { FirestoreSettings as LiteSettings } from '../lite-api/settings'; +import { FirestoreLocalCache } from './cache_config'; + export { DEFAULT_HOST } from '../lite-api/settings'; /** @@ -39,6 +41,9 @@ export interface PersistenceSettings { */ export interface FirestoreSettings extends LiteSettings { /** + * NOTE: This field will be deprecated in a future major release. Use `cache` field + * instead to specify cache size, and other cache configurations. + * * An approximate cache size threshold for the on-disk data. If the cache * grows beyond this size, Firestore will start removing data that hasn't been * recently used. The size is not a guarantee that the cache will stay below @@ -50,6 +55,18 @@ export interface FirestoreSettings extends LiteSettings { */ cacheSizeBytes?: number; + /** + * Specifies the cache used by the SDK. Availabe options are `MemoryLocalCache` + * and `IndexedDbLocalCache`, each with different configuration options. + * + * When unspecified, `MemoryLocalCache` will be used by default. + * + * NOTE: setting this field and `cacheSizeBytes` at the same time will throw + * exception during SDK initialization. Instead, using the configuration in + * the `FirestoreLocalCache` object to specify the cache size. + */ + localCache?: FirestoreLocalCache; + /** * Forces the SDK’s underlying network transport (WebChannel) to use * long-polling. Each response from the backend will be closed immediately diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index ae96b64fa6e..2d69b322db7 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -25,6 +25,7 @@ import { import { User } from '../auth/user'; import { LocalStore } from '../local/local_store'; import { + localStoreConfigureFieldIndexes, localStoreExecuteQuery, localStoreGetNamedQuery, localStoreHandleUserChange, @@ -33,6 +34,7 @@ import { import { Persistence } from '../local/persistence'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; +import { FieldIndex } from '../model/field_index'; import { Mutation } from '../model/mutation'; import { ObjectValue } from '../model/object_value'; import { toByteStreamReader } from '../platform/byte_stream_reader'; @@ -53,7 +55,7 @@ import { AsyncQueue, wrapInUserErrorIfRecoverable } from '../util/async_queue'; import { BundleReader } from '../util/bundle_reader'; import { newBundleReader } from '../util/bundle_reader_impl'; import { Code, FirestoreError } from '../util/error'; -import { logDebug } from '../util/log'; +import { logDebug, logWarn } from '../util/log'; import { AutoId } from '../util/misc'; import { Deferred } from '../util/promise'; @@ -94,10 +96,15 @@ import { ViewSnapshot } from './view_snapshot'; const LOG_TAG = 'FirestoreClient'; export const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100; +/** DOMException error code constants. */ +const DOM_EXCEPTION_INVALID_STATE = 11; +const DOM_EXCEPTION_ABORTED = 20; +const DOM_EXCEPTION_QUOTA_EXCEEDED = 22; + /** - * FirestoreClient is a top-level class that constructs and owns all of the - * pieces of the client SDK architecture. It is responsible for creating the - * async queue that is shared by all of the other components in the system. + * FirestoreClient is a top-level class that constructs and owns all of the // + * pieces of the client SDK architecture. It is responsible for creating the // + * async queue that is shared by all of the other components in the system. // */ export class FirestoreClient { private user = User.UNAUTHENTICATED; @@ -108,9 +115,14 @@ export class FirestoreClient { appCheckToken: string, user: User ) => Promise = () => Promise.resolve(); + _uninitializedComponentsProvider?: { + _offline: OfflineComponentProvider; + _offlineKind: 'memory' | 'persistent'; + _online: OnlineComponentProvider; + }; - offlineComponents?: OfflineComponentProvider; - onlineComponents?: OnlineComponentProvider; + _offlineComponents?: OfflineComponentProvider; + _onlineComponents?: OnlineComponentProvider; constructor( private authCredentials: CredentialsProvider, @@ -160,8 +172,8 @@ export class FirestoreClient { } /** - * Checks that the client has not been terminated. Ensures that other methods on - * this class cannot be called after the client is terminated. + * Checks that the client has not been terminated. Ensures that other methods on // + * this class cannot be called after the client is terminated. // */ verifyNotTerminated(): void { if (this.asyncQueue.isShuttingDown) { @@ -177,11 +189,11 @@ export class FirestoreClient { const deferred = new Deferred(); this.asyncQueue.enqueueAndForgetEvenWhileRestricted(async () => { try { - if (this.onlineComponents) { - await this.onlineComponents.terminate(); + if (this._onlineComponents) { + await this._onlineComponents.terminate(); } - if (this.offlineComponents) { - await this.offlineComponents.terminate(); + if (this._offlineComponents) { + await this._offlineComponents.terminate(); } // The credentials provider must be terminated after shutting down the @@ -229,7 +241,7 @@ export async function setOfflineComponentProvider( client.terminate() ); - client.offlineComponents = offlineComponentProvider; + client._offlineComponents = offlineComponentProvider; } export async function setOnlineComponentProvider( @@ -254,32 +266,102 @@ export async function setOnlineComponentProvider( client.setAppCheckTokenChangeListener((_, user) => remoteStoreHandleCredentialChange(onlineComponentProvider.remoteStore, user) ); - client.onlineComponents = onlineComponentProvider; + client._onlineComponents = onlineComponentProvider; +} + +/** + * Decides whether the provided error allows us to gracefully disable + * persistence (as opposed to crashing the client). + */ +export function canFallbackFromIndexedDbError( + error: FirestoreError | DOMException +): boolean { + if (error.name === 'FirebaseError') { + return ( + error.code === Code.FAILED_PRECONDITION || + error.code === Code.UNIMPLEMENTED + ); + } else if ( + typeof DOMException !== 'undefined' && + error instanceof DOMException + ) { + // There are a few known circumstances where we can open IndexedDb but + // trying to read/write will fail (e.g. quota exceeded). For + // well-understood cases, we attempt to detect these and then gracefully + // fall back to memory persistence. + // NOTE: Rather than continue to add to this list, we could decide to + // always fall back, with the risk that we might accidentally hide errors + // representing actual SDK bugs. + return ( + // When the browser is out of quota we could get either quota exceeded + // or an aborted error depending on whether the error happened during + // schema migration. + error.code === DOM_EXCEPTION_QUOTA_EXCEEDED || + error.code === DOM_EXCEPTION_ABORTED || + // Firefox Private Browsing mode disables IndexedDb and returns + // INVALID_STATE for any usage. + error.code === DOM_EXCEPTION_INVALID_STATE + ); + } + + return true; } async function ensureOfflineComponents( client: FirestoreClient ): Promise { - if (!client.offlineComponents) { - logDebug(LOG_TAG, 'Using default OfflineComponentProvider'); - await setOfflineComponentProvider( - client, - new MemoryOfflineComponentProvider() - ); + if (!client._offlineComponents) { + if (client._uninitializedComponentsProvider) { + logDebug(LOG_TAG, 'Using user provided OfflineComponentProvider'); + try { + await setOfflineComponentProvider( + client, + client._uninitializedComponentsProvider._offline + ); + } catch (e) { + const error = e as FirestoreError | DOMException; + if (!canFallbackFromIndexedDbError(error)) { + throw error; + } + logWarn( + 'Error using user provided cache. Falling back to ' + + 'memory cache: ' + + error + ); + await setOfflineComponentProvider( + client, + new MemoryOfflineComponentProvider() + ); + } + } else { + logDebug(LOG_TAG, 'Using default OfflineComponentProvider'); + await setOfflineComponentProvider( + client, + new MemoryOfflineComponentProvider() + ); + } } - return client.offlineComponents!; + return client._offlineComponents!; } async function ensureOnlineComponents( client: FirestoreClient ): Promise { - if (!client.onlineComponents) { - logDebug(LOG_TAG, 'Using default OnlineComponentProvider'); - await setOnlineComponentProvider(client, new OnlineComponentProvider()); + if (!client._onlineComponents) { + if (client._uninitializedComponentsProvider) { + logDebug(LOG_TAG, 'Using user provided OnlineComponentProvider'); + await setOnlineComponentProvider( + client, + client._uninitializedComponentsProvider._online + ); + } else { + logDebug(LOG_TAG, 'Using default OnlineComponentProvider'); + await setOnlineComponentProvider(client, new OnlineComponentProvider()); + } } - return client.onlineComponents!; + return client._onlineComponents!; } function getPersistence(client: FirestoreClient): Promise { @@ -744,3 +826,15 @@ function createBundleReader( } return newBundleReader(toByteStreamReader(content), serializer); } + +export function firestoreClientSetIndexConfiguration( + client: FirestoreClient, + indexes: FieldIndex[] +): Promise { + return client.asyncQueue.enqueue(async () => { + return localStoreConfigureFieldIndexes( + await getLocalStore(client), + indexes + ); + }); +} diff --git a/packages/firestore/src/lite-api/settings.ts b/packages/firestore/src/lite-api/settings.ts index 3743cc344d0..5f395846720 100644 --- a/packages/firestore/src/lite-api/settings.ts +++ b/packages/firestore/src/lite-api/settings.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { FirestoreLocalCache } from '../api/cache_config'; import { CredentialsSettings } from '../api/credentials'; import { LRU_COLLECTION_DISABLED, @@ -60,6 +61,8 @@ export interface PrivateSettings extends FirestoreSettings { experimentalAutoDetectLongPolling?: boolean; // Used in firestore@exp useFetchStreams?: boolean; + + localCache?: FirestoreLocalCache; } /** @@ -83,6 +86,7 @@ export class FirestoreSettingsImpl { readonly ignoreUndefinedProperties: boolean; readonly useFetchStreams: boolean; + readonly cache?: FirestoreLocalCache; // Can be a google-auth-library or gapi client. // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -105,6 +109,7 @@ export class FirestoreSettingsImpl { this.credentials = settings.credentials; this.ignoreUndefinedProperties = !!settings.ignoreUndefinedProperties; + this.cache = settings.localCache; if (settings.cacheSizeBytes === undefined) { this.cacheSizeBytes = LRU_DEFAULT_CACHE_SIZE_BYTES; diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 5f3e2dc6c61..174048020e6 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -1214,6 +1214,7 @@ apiDescribe('Database', (persistence: boolean) => { 'cannot clear persistence if the client has been initialized', async () => { await withTestDoc(persistence, async (docRef, firestore) => { + await setDoc(docRef, {}); const expectedError = 'Persistence can only be cleared before a Firestore instance is ' + 'initialized or after it is terminated.'; diff --git a/packages/firestore/test/integration/api/provider.test.ts b/packages/firestore/test/integration/api/provider.test.ts index ffcd0e7f350..04a148bf987 100644 --- a/packages/firestore/test/integration/api/provider.test.ts +++ b/packages/firestore/test/integration/api/provider.test.ts @@ -25,7 +25,11 @@ import { initializeFirestore, Firestore, terminate, - getDoc + getDoc, + enableIndexedDbPersistence, + setDoc, + memoryLocalCache, + getDocFromCache } from '../util/firebase_export'; import { DEFAULT_SETTINGS } from '../util/settings'; @@ -120,6 +124,37 @@ describe('Firestore Provider', () => { expect(fs1).to.be.equal(fs2); }); + it('can still use enableIndexedDbPersistence()', async () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-use-enablePersistence' + ); + const db = initializeFirestore(app, DEFAULT_SETTINGS); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(enableIndexedDbPersistence(db)).to.be.rejected; + + // SDK still functions. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + setDoc(doc(db, 'coll/doc'), { field: 'foo' }); + expect((await getDocFromCache(doc(db, 'coll/doc'))).data()).to.deep.equal({ + field: 'foo' + }); + }); + + it('cannot mix enableIndexedDbPersistence() and settings.cache', async () => { + const app = initializeApp( + { apiKey: 'fake-api-key', projectId: 'test-project' }, + 'test-cannot-mix' + ); + const db = initializeFirestore(app, { + ...DEFAULT_SETTINGS, + localCache: memoryLocalCache() + }); + expect(() => enableIndexedDbPersistence(db)).to.throw( + 'SDK cache is already specified.' + ); + }); + it('cannot use once terminated', () => { const app = initializeApp( { apiKey: 'fake-api-key', projectId: 'test-project' }, diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index 8ec14d1ae3e..f422eb4ad07 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -240,9 +240,7 @@ apiDescribe('Validation:', (persistence: boolean) => { doc(db, 'foo/bar'); } expect(() => enableIndexedDbPersistence(db)).to.throw( - 'Firestore has already been started and persistence can no ' + - 'longer be enabled. You can only enable persistence before ' + - 'calling any other methods on a Firestore object.' + 'SDK cache is already specified.' ); } ); diff --git a/packages/firestore/test/integration/browser/indexeddb.test.ts b/packages/firestore/test/integration/browser/indexeddb.test.ts index a63d5178118..b00fd7fbb83 100644 --- a/packages/firestore/test/integration/browser/indexeddb.test.ts +++ b/packages/firestore/test/integration/browser/indexeddb.test.ts @@ -26,7 +26,7 @@ import { } from '../util/firebase_export'; import { isPersistenceAvailable, withTestDb } from '../util/helpers'; -describe('where persistence is unsupported, enablePersistence', () => { +describe('where indexeddb is not available: ', () => { // Only test on platforms where persistence is *not* available (e.g. Edge, // Node.JS). if (isPersistenceAvailable()) { @@ -61,4 +61,14 @@ describe('where persistence is unsupported, enablePersistence', () => { ); }); }); + + it('fails back to memory cache with initializeFirestore too', () => { + // withTestDb will fail the test if persistence is requested but it fails + // so we'll enable persistence here instead. + return withTestDb(/* persistence= */ true, db => { + // Do the set immediately without waiting on the promise. + const testDoc = doc(collection(db, 'test-collection')); + return setDoc(testDoc, { foo: 'bar' }); + }); + }); }); diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 79dbacaafa0..2d579afeb5f 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -23,8 +23,8 @@ import { DocumentReference, Firestore, terminate, + persistentLocalCache, clearIndexedDbPersistence, - enableIndexedDbPersistence, CollectionReference, DocumentData, QuerySnapshot, @@ -184,10 +184,11 @@ export async function withTestDbsSettings( const dbs: Firestore[] = []; for (let i = 0; i < numDbs; i++) { - const db = newTestFirestore(newTestApp(projectId), settings); + const newSettings = { ...settings }; if (persistence) { - await enableIndexedDbPersistence(db); + newSettings.localCache = persistentLocalCache(); } + const db = newTestFirestore(newTestApp(projectId), newSettings); dbs.push(db); } @@ -218,10 +219,11 @@ export async function withNamedTestDbsOrSkipUnlessUsingEmulator( const app = newTestApp(DEFAULT_PROJECT_ID); const dbs: Firestore[] = []; for (const dbName of dbNames) { - const db = newTestFirestore(app, DEFAULT_SETTINGS, dbName); + const newSettings = { ...DEFAULT_SETTINGS }; if (persistence) { - await enableIndexedDbPersistence(db); + newSettings.localCache = persistentLocalCache(); } + const db = newTestFirestore(app, newSettings, dbName); dbs.push(db); } diff --git a/packages/firestore/test/unit/local/local_store_indexeddb.test.ts b/packages/firestore/test/unit/local/local_store_indexeddb.test.ts index f10fa13c1d7..530ca40b5e4 100644 --- a/packages/firestore/test/unit/local/local_store_indexeddb.test.ts +++ b/packages/firestore/test/unit/local/local_store_indexeddb.test.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { isIndexedDBAvailable } from '@firebase/util'; import { expect } from 'chai'; import { serverTimestamp, Timestamp } from '../../../src'; @@ -188,6 +189,10 @@ class AsyncLocalStoreTester { } describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { + if (!isIndexedDBAvailable()) { + return; + } + let persistence: Persistence; let test: AsyncLocalStoreTester;