diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 5d682599ef4..0f3a94a4be8 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -55,7 +55,20 @@ import { OfflineComponentProvider, OnlineComponentProvider } from '../core/component_provider'; -import { FirestoreClient } from '../core/firestore_client'; +import { + FirestoreClient, + firestoreClientAddSnapshotsInSyncListener, + firestoreClientDisableNetwork, + firestoreClientEnableNetwork, + firestoreClientGetDocumentFromLocalCache, + firestoreClientGetDocumentsFromLocalCache, + firestoreClientGetDocumentsViaSnapshotListener, + firestoreClientGetDocumentViaSnapshotListener, + firestoreClientListen, + firestoreClientTransaction, + firestoreClientWaitForPendingWrites, + firestoreClientWrite +} from '../core/firestore_client'; import { Bound, Direction, @@ -101,7 +114,7 @@ import { validateSetOptions, valueDescription } from '../util/input_validation'; -import { setLogLevel as setClientLogLevel, logWarn } from '../util/log'; +import { logWarn, setLogLevel as setClientLogLevel } from '../util/log'; import { AutoId } from '../util/misc'; import { Deferred } from '../util/promise'; import { FieldPath as ExternalFieldPath } from './field_path'; @@ -461,12 +474,12 @@ export class Firestore implements PublicFirestore, FirebaseService { enableNetwork(): Promise { this.ensureClientConfigured(); - return this._firestoreClient!.enableNetwork(); + return firestoreClientEnableNetwork(this._firestoreClient!); } disableNetwork(): Promise { this.ensureClientConfigured(); - return this._firestoreClient!.disableNetwork(); + return firestoreClientDisableNetwork(this._firestoreClient!); } enablePersistence(settings?: PublicPersistenceSettings): Promise { @@ -528,7 +541,7 @@ export class Firestore implements PublicFirestore, FirebaseService { waitForPendingWrites(): Promise { this.ensureClientConfigured(); - return this._firestoreClient!.waitForPendingWrites(); + return firestoreClientWaitForPendingWrites(this._firestoreClient!); } onSnapshotsInSync(observer: PartialObserver): Unsubscribe; @@ -537,14 +550,18 @@ export class Firestore implements PublicFirestore, FirebaseService { this.ensureClientConfigured(); if (isPartialObserver(arg)) { - return this._firestoreClient!.addSnapshotsInSyncListener( + return firestoreClientAddSnapshotsInSyncListener( + this._firestoreClient!, arg as PartialObserver ); } else { const observer: PartialObserver = { next: arg as () => void }; - return this._firestoreClient!.addSnapshotsInSyncListener(observer); + return firestoreClientAddSnapshotsInSyncListener( + this._firestoreClient!, + observer + ); } } @@ -676,7 +693,9 @@ export class Firestore implements PublicFirestore, FirebaseService { runTransaction( updateFunction: (transaction: PublicTransaction) => Promise ): Promise { - return this.ensureClientConfigured().transaction( + const firestoreClient = this.ensureClientConfigured(); + return firestoreClientTransaction( + firestoreClient, (transaction: InternalTransaction) => { return updateFunction(new Transaction(this, transaction)); } @@ -685,7 +704,6 @@ export class Firestore implements PublicFirestore, FirebaseService { batch(): PublicWriteBatch { this.ensureClientConfigured(); - return new WriteBatch(this); } @@ -966,7 +984,8 @@ export class WriteBatch implements PublicWriteBatch { this.verifyNotCommitted(); this._committed = true; if (this._mutations.length > 0) { - return this._firestore.ensureClientConfigured().write(this._mutations); + const firestoreClient = this._firestore.ensureClientConfigured(); + return firestoreClientWrite(firestoreClient, this._mutations); } return Promise.resolve(); @@ -1080,7 +1099,8 @@ export class DocumentReference this._converter !== null, options ); - return this._firestoreClient.write( + return firestoreClientWrite( + this._firestoreClient, parsed.toMutations(this._key, Precondition.none()) ); } @@ -1119,13 +1139,14 @@ export class DocumentReference ); } - return this._firestoreClient.write( + return firestoreClientWrite( + this._firestoreClient, parsed.toMutations(this._key, Precondition.exists(true)) ); } delete(): Promise { - return this._firestoreClient.write([ + return firestoreClientWrite(this._firestoreClient, [ new DeleteMutation(this._key, Precondition.none()) ]); } @@ -1185,7 +1206,8 @@ export class DocumentReference complete: args[currArg + 2] as CompleteFn }; - return this._firestoreClient.listen( + return firestoreClientListen( + this._firestoreClient, newQueryForPath(this._key.path), internalOptions, observer @@ -1195,23 +1217,26 @@ export class DocumentReference get(options?: GetOptions): Promise> { const firestoreClient = this.firestore.ensureClientConfigured(); if (options && options.source === 'cache') { - return firestoreClient - .getDocumentFromLocalCache(this._key) - .then( - doc => - new DocumentSnapshot( - this.firestore, - this._key, - doc, - /*fromCache=*/ true, - doc instanceof Document ? doc.hasLocalMutations : false, - this._converter - ) - ); + return firestoreClientGetDocumentFromLocalCache( + firestoreClient, + this._key + ).then( + doc => + new DocumentSnapshot( + this.firestore, + this._key, + doc, + /*fromCache=*/ true, + doc instanceof Document ? doc.hasLocalMutations : false, + this._converter + ) + ); } else { - return firestoreClient - .getDocumentViaSnapshotListener(this._key, options) - .then(snapshot => this._convertToDocSnapshot(snapshot)); + return firestoreClientGetDocumentViaSnapshotListener( + firestoreClient, + this._key, + options + ).then(snapshot => this._convertToDocSnapshot(snapshot)); } } @@ -2036,7 +2061,12 @@ export class Query implements PublicQuery { validateHasExplicitOrderByForLimitToLast(this._query); const firestoreClient = this.firestore.ensureClientConfigured(); - return firestoreClient.listen(this._query, options, observer); + return firestoreClientListen( + firestoreClient, + this._query, + options, + observer + ); } get(options?: GetOptions): Promise> { @@ -2045,8 +2075,12 @@ export class Query implements PublicQuery { const firestoreClient = this.firestore.ensureClientConfigured(); return (options && options.source === 'cache' - ? firestoreClient.getDocumentsFromLocalCache(this._query) - : firestoreClient.getDocumentsViaSnapshotListener(this._query, options) + ? firestoreClientGetDocumentsFromLocalCache(firestoreClient, this._query) + : firestoreClientGetDocumentsViaSnapshotListener( + firestoreClient, + this._query, + options + ) ).then( snap => new QuerySnapshot(this.firestore, this._query, snap, this._converter) diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index e8a4ac3467f..869c382f0d1 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -90,12 +90,12 @@ export class FirestoreClient { // with the types rather than littering the code with '!' or unnecessary // undefined checks. private databaseInfo!: DatabaseInfo; - private eventMgr!: EventManager; - private persistence!: Persistence; - private localStore!: LocalStore; - private datastore!: Datastore; - private remoteStore!: RemoteStore; - private syncEngine!: SyncEngine; + eventMgr!: EventManager; + persistence!: Persistence; + localStore!: LocalStore; + datastore!: Datastore; + remoteStore!: RemoteStore; + syncEngine!: SyncEngine; private gcScheduler!: GarbageCollectionScheduler | null; // PORTING NOTE: SharedClientState is only used for multi-tab web. @@ -110,7 +110,7 @@ export class FirestoreClient { // // If initializationDone resolved then the FirestoreClient is in a usable // state. - private readonly initializationDone = new Deferred(); + readonly initializationDone = new Deferred(); constructor( private credentials: CredentialsProvider, @@ -122,7 +122,7 @@ export class FirestoreClient { * start processing a new operation while the previous one is waiting for * an async I/O to complete). */ - private asyncQueue: AsyncQueue + public asyncQueue: AsyncQueue ) {} /** @@ -209,15 +209,6 @@ export class FirestoreClient { return persistenceResult.promise; } - /** Enables the network connection and requeues all pending operations. */ - enableNetwork(): Promise { - this.verifyNotTerminated(); - return this.asyncQueue.enqueue(() => { - this.persistence.setNetworkEnabled(true); - return remoteStoreEnableNetwork(this.remoteStore); - }); - } - /** * Initializes persistent storage, attempting to use IndexedDB if * usePersistence is true or memory-only if false. @@ -307,7 +298,7 @@ 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. */ - private verifyNotTerminated(): void { + verifyNotTerminated(): void { if (this.asyncQueue.isShuttingDown) { throw new FirestoreError( Code.FAILED_PRECONDITION, @@ -316,15 +307,6 @@ export class FirestoreClient { } } - /** Disables the network connection. Pending operations will not complete. */ - disableNetwork(): Promise { - this.verifyNotTerminated(); - return this.asyncQueue.enqueue(() => { - this.persistence.setNetworkEnabled(false); - return remoteStoreDisableNetwork(this.remoteStore); - }); - } - terminate(): Promise { this.asyncQueue.enterRestrictedMode(); const deferred = new Deferred(); @@ -355,165 +337,203 @@ export class FirestoreClient { return deferred.promise; } - /** - * Returns a Promise that resolves when all writes that were pending at the time this - * method was called received server acknowledgement. An acknowledgement can be either acceptance - * or rejection. - */ - waitForPendingWrites(): Promise { - this.verifyNotTerminated(); - - const deferred = new Deferred(); - this.asyncQueue.enqueueAndForget(() => - registerPendingWritesCallback(this.syncEngine, deferred) - ); - return deferred.promise; + databaseId(): DatabaseId { + return this.databaseInfo.databaseId; } - listen( - query: Query, - options: ListenOptions, - observer: Partial> - ): () => void { - this.verifyNotTerminated(); - const wrappedObserver = new AsyncObserver(observer); - const listener = new QueryListener(query, wrappedObserver, options); - this.asyncQueue.enqueueAndForget(() => - eventManagerListen(this.eventMgr, listener) - ); - return () => { - wrappedObserver.mute(); - this.asyncQueue.enqueueAndForget(() => - eventManagerUnlisten(this.eventMgr, listener) - ); - }; + get clientTerminated(): boolean { + // Technically, the asyncQueue is still running, but only accepting operations + // related to termination or supposed to be run after termination. It is effectively + // terminated to the eyes of users. + return this.asyncQueue.isShuttingDown; } +} - async getDocumentFromLocalCache( - docKey: DocumentKey - ): Promise { - this.verifyNotTerminated(); - await this.initializationDone.promise; - const deferred = new Deferred(); - this.asyncQueue.enqueueAndForget(() => - readDocumentFromCache(this.localStore, docKey, deferred) - ); - return deferred.promise; - } +/** Enables the network connection and requeues all pending operations. */ +export function firestoreClientEnableNetwork( + firestoreClient: FirestoreClient +): Promise { + firestoreClient.verifyNotTerminated(); + return firestoreClient.asyncQueue.enqueue(() => { + firestoreClient.persistence.setNetworkEnabled(true); + return remoteStoreEnableNetwork(firestoreClient.remoteStore); + }); +} - async getDocumentViaSnapshotListener( - key: DocumentKey, - options: GetOptions = {} - ): Promise { - this.verifyNotTerminated(); - await this.initializationDone.promise; - const deferred = new Deferred(); - this.asyncQueue.enqueueAndForget(() => - readDocumentViaSnapshotListener( - this.eventMgr, - this.asyncQueue, - key, - options, - deferred - ) - ); - return deferred.promise; - } +/** Disables the network connection. Pending operations will not complete. */ +export function firestoreClientDisableNetwork( + firestoreClient: FirestoreClient +): Promise { + firestoreClient.verifyNotTerminated(); + return firestoreClient.asyncQueue.enqueue(() => { + firestoreClient.persistence.setNetworkEnabled(false); + return remoteStoreDisableNetwork(firestoreClient.remoteStore); + }); +} - async getDocumentsFromLocalCache(query: Query): Promise { - this.verifyNotTerminated(); - await this.initializationDone.promise; - const deferred = new Deferred(); - this.asyncQueue.enqueueAndForget(() => - executeQueryFromCache(this.localStore, query, deferred) - ); - return deferred.promise; - } +/** + * Returns a Promise that resolves when all writes that were pending at the time this + * method was called received server acknowledgement. An acknowledgement can be either acceptance + * or rejection. + */ +export function firestoreClientWaitForPendingWrites( + firestoreClient: FirestoreClient +): Promise { + firestoreClient.verifyNotTerminated(); - async getDocumentsViaSnapshotListener( - query: Query, - options: GetOptions = {} - ): Promise { - this.verifyNotTerminated(); - await this.initializationDone.promise; - const deferred = new Deferred(); - this.asyncQueue.enqueueAndForget(() => - executeQueryViaSnapshotListener( - this.eventMgr, - this.asyncQueue, - query, - options, - deferred - ) - ); - return deferred.promise; - } + const deferred = new Deferred(); + firestoreClient.asyncQueue.enqueueAndForget(() => + registerPendingWritesCallback(firestoreClient.syncEngine, deferred) + ); + return deferred.promise; +} - write(mutations: Mutation[]): Promise { - this.verifyNotTerminated(); - const deferred = new Deferred(); - this.asyncQueue.enqueueAndForget(() => - syncEngineWrite(this.syncEngine, mutations, deferred) +export function firestoreClientListen( + firestoreClient: FirestoreClient, + query: Query, + options: ListenOptions, + observer: Partial> +): () => void { + firestoreClient.verifyNotTerminated(); + const wrappedObserver = new AsyncObserver(observer); + const listener = new QueryListener(query, wrappedObserver, options); + firestoreClient.asyncQueue.enqueueAndForget(() => + eventManagerListen(firestoreClient.eventMgr, listener) + ); + return () => { + wrappedObserver.mute(); + firestoreClient.asyncQueue.enqueueAndForget(() => + eventManagerUnlisten(firestoreClient.eventMgr, listener) ); - return deferred.promise; - } + }; +} - databaseId(): DatabaseId { - return this.databaseInfo.databaseId; - } +export async function firestoreClientGetDocumentFromLocalCache( + firestoreClient: FirestoreClient, + docKey: DocumentKey +): Promise { + firestoreClient.verifyNotTerminated(); + await firestoreClient.initializationDone.promise; + const deferred = new Deferred(); + firestoreClient.asyncQueue.enqueueAndForget(() => + readDocumentFromCache(firestoreClient.localStore, docKey, deferred) + ); + return deferred.promise; +} - addSnapshotsInSyncListener(observer: Partial>): () => void { - this.verifyNotTerminated(); - const wrappedObserver = new AsyncObserver(observer); - this.asyncQueue.enqueueAndForget(async () => - addSnapshotsInSyncListener(this.eventMgr, wrappedObserver) - ); - return () => { - wrappedObserver.mute(); - this.asyncQueue.enqueueAndForget(async () => - removeSnapshotsInSyncListener(this.eventMgr, wrappedObserver) - ); - }; - } +export async function firestoreClientGetDocumentViaSnapshotListener( + firestoreClient: FirestoreClient, + key: DocumentKey, + options: GetOptions = {} +): Promise { + firestoreClient.verifyNotTerminated(); + await firestoreClient.initializationDone.promise; + const deferred = new Deferred(); + firestoreClient.asyncQueue.enqueueAndForget(() => + readDocumentViaSnapshotListener( + firestoreClient.eventMgr, + firestoreClient.asyncQueue, + key, + options, + deferred + ) + ); + return deferred.promise; +} - get clientTerminated(): boolean { - // Technically, the asyncQueue is still running, but only accepting operations - // related to termination or supposed to be run after termination. It is effectively - // terminated to the eyes of users. - return this.asyncQueue.isShuttingDown; - } +export async function firestoreClientGetDocumentsFromLocalCache( + firestoreClient: FirestoreClient, + query: Query +): Promise { + firestoreClient.verifyNotTerminated(); + await firestoreClient.initializationDone.promise; + const deferred = new Deferred(); + firestoreClient.asyncQueue.enqueueAndForget(() => + executeQueryFromCache(firestoreClient.localStore, query, deferred) + ); + return deferred.promise; +} - /** - * Takes an updateFunction in which a set of reads and writes can be performed - * atomically. In the updateFunction, the client can read and write values - * using the supplied transaction object. After the updateFunction, all - * changes will be committed. If a retryable error occurs (ex: some other - * client has changed any of the data referenced), then the updateFunction - * will be called again after a backoff. If the updateFunction still fails - * after all retries, then the transaction will be rejected. - * - * The transaction object passed to the updateFunction contains methods for - * accessing documents and collections. Unlike other datastore access, data - * accessed with the transaction will not reflect local changes that have not - * been committed. For this reason, it is required that all reads are - * performed before any writes. Transactions must be performed while online. - */ - transaction( - updateFunction: (transaction: Transaction) => Promise - ): Promise { - this.verifyNotTerminated(); - const deferred = new Deferred(); - this.asyncQueue.enqueueAndForget(() => { - new TransactionRunner( - this.asyncQueue, - this.datastore, - updateFunction, - deferred - ).run(); - return Promise.resolve(); - }); - return deferred.promise; - } +export async function firestoreClientGetDocumentsViaSnapshotListener( + firestoreClient: FirestoreClient, + query: Query, + options: GetOptions = {} +): Promise { + firestoreClient.verifyNotTerminated(); + await firestoreClient.initializationDone.promise; + const deferred = new Deferred(); + firestoreClient.asyncQueue.enqueueAndForget(() => + executeQueryViaSnapshotListener( + firestoreClient.eventMgr, + firestoreClient.asyncQueue, + query, + options, + deferred + ) + ); + return deferred.promise; +} + +export function firestoreClientWrite( + firestoreClient: FirestoreClient, + mutations: Mutation[] +): Promise { + firestoreClient.verifyNotTerminated(); + const deferred = new Deferred(); + firestoreClient.asyncQueue.enqueueAndForget(() => + syncEngineWrite(firestoreClient.syncEngine, mutations, deferred) + ); + return deferred.promise; +} + +export function firestoreClientAddSnapshotsInSyncListener( + firestoreClient: FirestoreClient, + observer: Partial> +): () => void { + firestoreClient.verifyNotTerminated(); + const wrappedObserver = new AsyncObserver(observer); + firestoreClient.asyncQueue.enqueueAndForget(async () => + addSnapshotsInSyncListener(firestoreClient.eventMgr, wrappedObserver) + ); + return () => { + wrappedObserver.mute(); + firestoreClient.asyncQueue.enqueueAndForget(async () => + removeSnapshotsInSyncListener(firestoreClient.eventMgr, wrappedObserver) + ); + }; +} + +/** + * Takes an updateFunction in which a set of reads and writes can be performed + * atomically. In the updateFunction, the client can read and write values + * using the supplied transaction object. After the updateFunction, all + * changes will be committed. If a retryable error occurs (ex: some other + * client has changed any of the data referenced), then the updateFunction + * will be called again after a backoff. If the updateFunction still fails + * after all retries, then the transaction will be rejected. + * + * The transaction object passed to the updateFunction contains methods for + * accessing documents and collections. Unlike other datastore access, data + * accessed with the transaction will not reflect local changes that have not + * been committed. For this reason, it is required that all reads are + * performed before any writes. Transactions must be performed while online. + */ +export function firestoreClientTransaction( + firestoreClient: FirestoreClient, + updateFunction: (transaction: Transaction) => Promise +): Promise { + firestoreClient.verifyNotTerminated(); + const deferred = new Deferred(); + firestoreClient.asyncQueue.enqueueAndForget(() => { + new TransactionRunner( + firestoreClient.asyncQueue, + firestoreClient.datastore, + updateFunction, + deferred + ).run(); + return Promise.resolve(); + }); + return deferred.promise; } export async function readDocumentFromCache(