diff --git a/dev/src/index.ts b/dev/src/index.ts index cd79b3332..5d90c452f 100644 --- a/dev/src/index.ts +++ b/dev/src/index.ts @@ -67,6 +67,7 @@ import { validateMinNumberOfArguments, validateObject, validateString, + validateTimestamp, } from './validate'; import {WriteBatch} from './write-batch'; @@ -929,13 +930,37 @@ export class Firestore implements firestore.Firestore { * * @callback Firestore~updateFunction * @template T - * @param {Transaction} transaction The transaction object for this + * @param {Transaction} transaction The transaction object for this * transaction. * @returns {Promise<T>} The promise returned at the end of the transaction. * This promise will be returned by {@link Firestore#runTransaction} if the * transaction completed successfully. */ + /** + * Options object for {@link Firestore#runTransaction} to configure a + * read-only transaction. + * + * @callback Firestore~ReadOnlyTransactionOptions + * @template T + * @param {true} readOnly Set to true to indicate a read-only transaction. + * @param {Timestamp=} readTime If specified, documents are read at the given + * time. This may not be more than 60 seconds in the past from when the + * request is processed by the server. + */ + + /** + * Options object for {@link Firestore#runTransaction} to configure a + * read-write transaction. + * + * @callback Firestore~ReadWriteTransactionOptions + * @template T + * @param {false=} readOnly Set to false or omit to indicate a read-write + * transaction. + * @param {number=} maxAttempts The maximum number of attempts for this + * transaction. Defaults to five. + */ + /** * Executes the given updateFunction and commits the changes applied within * the transaction. @@ -944,26 +969,33 @@ export class Firestore implements firestore.Firestore { * modify Firestore documents under lock. You have to perform all reads before * before you perform any write. * - * Documents read during a transaction are locked pessimistically. A - * transaction's lock on a document blocks other transactions, batched - * writes, and other non-transactional writes from changing that document. - * A transaction releases its document locks at commit time or once it times - * out or fails for any reason. + * Transactions can be performed as read-only or read-write transactions. By + * default, transactions are executed in read-write mode. + * + * A read-write transaction obtains a pessimistic lock on all documents that + * are read during the transaction. These locks block other transactions, + * batched writes, and other non-transactional writes from changing that + * document. Any writes in a read-write transactions are committed once + * 'updateFunction' resolves, which also releases all locks. + * + * If a read-write transaction fails with contention, the transaction is + * retried up to five times. The `updateFunction` is invoked once for each + * attempt. * - * Transactions are committed once 'updateFunction' resolves. If a transaction - * fails with contention, the transaction is retried up to five times. The - * `updateFunction` is invoked once for each attempt. + * Read-only transactions do not lock documents. They can be used to read + * documents at a consistent snapshot in time, which may be up to 60 seconds + * in the past. Read-only transactions are not retried. * * Transactions time out after 60 seconds if no documents are read. * Transactions that are not committed within than 270 seconds are also - * aborted. + * aborted. Any remaining locks are released when a transaction times out. * * @template T * @param {Firestore~updateFunction} updateFunction The user function to * execute within the transaction context. - * @param {object=} transactionOptions Transaction options. - * @param {number=} transactionOptions.maxAttempts - The maximum number of - * attempts for this transaction. + * @param { + * Firestore~ReadWriteTransactionOptions|Firestore~ReadOnlyTransactionOptions= + * } transactionOptions Transaction options. * @returns {Promise<T>} If the transaction completed successfully or was * explicitly aborted (by the updateFunction returning a failed Promise), the * Promise returned by the updateFunction will be returned here. Else if the @@ -994,28 +1026,55 @@ export class Firestore implements firestore.Firestore { */ runTransaction<T>( updateFunction: (transaction: Transaction) => Promise<T>, - transactionOptions?: {maxAttempts?: number} + transactionOptions?: + | firestore.ReadWriteTransactionOptions + | firestore.ReadOnlyTransactionOptions ): Promise<T> { validateFunction('updateFunction', updateFunction); const tag = requestTag(); let maxAttempts = DEFAULT_MAX_TRANSACTION_ATTEMPTS; + let readOnly = false; + let readTime: Timestamp | undefined; if (transactionOptions) { validateObject('transactionOptions', transactionOptions); - validateInteger( - 'transactionOptions.maxAttempts', - transactionOptions.maxAttempts, - {optional: true, minValue: 1} + validateBoolean( + 'transactionOptions.readOnly', + transactionOptions.readOnly, + {optional: true} ); - maxAttempts = - transactionOptions.maxAttempts || DEFAULT_MAX_TRANSACTION_ATTEMPTS; + + if (transactionOptions.readOnly) { + validateTimestamp( + 'transactionOptions.readTime', + transactionOptions.readTime, + {optional: true} + ); + + readOnly = true; + readTime = transactionOptions.readTime as Timestamp | undefined; + maxAttempts = 1; + } else { + validateInteger( + 'transactionOptions.maxAttempts', + transactionOptions.maxAttempts, + {optional: true, minValue: 1} + ); + + maxAttempts = + transactionOptions.maxAttempts || DEFAULT_MAX_TRANSACTION_ATTEMPTS; + } } const transaction = new Transaction(this, tag); return this.initializeIfNeeded(tag).then(() => - transaction.runTransaction(updateFunction, maxAttempts) + transaction.runTransaction(updateFunction, { + maxAttempts, + readOnly, + readTime, + }) ); } diff --git a/dev/src/transaction.ts b/dev/src/transaction.ts index 678f6cc4d..7274a9cb0 100644 --- a/dev/src/transaction.ts +++ b/dev/src/transaction.ts @@ -22,6 +22,7 @@ import * as proto from '../protos/firestore_v1_proto_api'; import {ExponentialBackoff} from './backoff'; import {DocumentSnapshot} from './document'; import {Firestore, WriteBatch} from './index'; +import {Timestamp} from './timestamp'; import {logger} from './logger'; import {FieldPath, validateFieldPath} from './path'; import {StatusCode} from './status-code'; @@ -346,12 +347,18 @@ export class Transaction implements firestore.Transaction { * * @private */ - begin(): Promise<void> { + begin(readOnly: boolean, readTime: Timestamp | undefined): Promise<void> { const request: api.IBeginTransactionRequest = { database: this._firestore.formattedName, }; - if (this._transactionId) { + if (readOnly) { + request.options = { + readOnly: { + readTime: readTime?.toProto()?.timestampValue, + }, + }; + } else if (this._transactionId) { request.options = { readWrite: { retryTransaction: this._transactionId, @@ -406,16 +413,20 @@ export class Transaction implements firestore.Transaction { * context. * @param requestTag A unique client-assigned identifier for the scope of * this transaction. - * @param maxAttempts The maximum number of attempts for this transaction. + * @param options The user-defined options for this transaction. */ async runTransaction<T>( updateFunction: (transaction: Transaction) => Promise<T>, - maxAttempts: number + options: { + maxAttempts: number; + readOnly: boolean; + readTime?: Timestamp; + } ): Promise<T> { let result: T; let lastError: GoogleError | undefined = undefined; - for (let attempt = 0; attempt < maxAttempts; ++attempt) { + for (let attempt = 0; attempt < options.maxAttempts; ++attempt) { try { if (lastError) { logger( @@ -430,7 +441,7 @@ export class Transaction implements firestore.Transaction { this._writeBatch._reset(); await this.maybeBackoff(lastError); - await this.begin(); + await this.begin(options.readOnly, options.readTime); const promise = updateFunction(this); if (!(promise instanceof Promise)) { diff --git a/dev/src/validate.ts b/dev/src/validate.ts index 3db6390f1..0270b5f50 100644 --- a/dev/src/validate.ts +++ b/dev/src/validate.ts @@ -17,6 +17,7 @@ import {URL} from 'url'; import {FieldPath} from './path'; import {isFunction, isObject} from './util'; +import {Timestamp} from './timestamp'; /** * Options to allow argument omission. @@ -278,6 +279,26 @@ export function validateInteger( } } +/** + * Validates that 'value' is a Timestamp. + * + * @private + * @param arg The argument name or argument index (for varargs methods). + * @param value The input to validate. + * @param options Options that specify whether the Timestamp can be omitted. + */ +export function validateTimestamp( + arg: string | number, + value: unknown, + options?: RequiredArgumentOptions +): void { + if (!validateOptional(value, options)) { + if (!(value instanceof Timestamp)) { + throw new Error(invalidArgumentMessage(arg, 'Timestamp')); + } + } +} + /** * Generates an error message to use with invalid arguments. * diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 10379e830..11e45e1aa 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -2345,6 +2345,47 @@ describe('Transaction class', () => { const finalSnapshot = await ref.get(); expect(finalSnapshot.data()).to.deep.equal({first: true, second: true}); }); + + it('supports read-only transactions', async () => { + const ref = randomCol.doc('doc'); + await ref.set({foo: 'bar'}); + const snapshot = await firestore.runTransaction( + updateFunction => updateFunction.get(ref), + {readOnly: true} + ); + expect(snapshot.exists).to.be.true; + }); + + it('supports read-only transactions with custom read-time', async () => { + const ref = randomCol.doc('doc'); + const writeResult = await ref.set({foo: 1}); + await ref.set({foo: 2}); + const snapshot = await firestore.runTransaction( + updateFunction => updateFunction.get(ref), + {readOnly: true, readTime: writeResult.writeTime} + ); + expect(snapshot.exists).to.be.true; + expect(snapshot.get('foo')).to.equal(1); + }); + + it('fails read-only with writes', async () => { + let attempts = 0; + + const ref = randomCol.doc('doc'); + try { + await firestore.runTransaction( + async updateFunction => { + ++attempts; + updateFunction.set(ref, {}); + }, + {readOnly: true} + ); + expect.fail(); + } catch (e) { + expect(attempts).to.equal(1); + expect(e.code).to.equal(Status.INVALID_ARGUMENT); + } + }); }); describe('WriteBatch class', () => { diff --git a/dev/test/transaction.ts b/dev/test/transaction.ts index 5434c86ff..8854bc159 100644 --- a/dev/test/transaction.ts +++ b/dev/test/transaction.ts @@ -22,7 +22,7 @@ import * as through2 from 'through2'; import * as proto from '../protos/firestore_v1_proto_api'; import * as Firestore from '../src'; -import {DocumentReference, FieldPath, Transaction} from '../src'; +import {DocumentReference, FieldPath, Timestamp, Transaction} from '../src'; import {setTimeoutHandler} from '../src/backoff'; import { ApiOverride, @@ -35,6 +35,10 @@ import { } from './util/helpers'; import api = proto.google.firestore.v1; +import { + ReadOnlyTransactionOptions, + ReadWriteTransactionOptions, +} from '@google-cloud/firestore'; use(chaiAsPromised); @@ -128,29 +132,38 @@ function rollback( }; } -function begin( - transaction?: Uint8Array | string, - prevTransaction?: Uint8Array | string, - error?: Error -): TransactionStep { +function begin(options?: { + transactionId?: Uint8Array | string; + readOnly?: {readTime?: {seconds?: number; nanos?: number}}; + readWrite?: { + prevTransactionId?: Uint8Array | string; + }; + error?: Error; +}): TransactionStep { const proto: api.IBeginTransactionRequest = {database: DATABASE_ROOT}; - if (prevTransaction) { + if (options?.readOnly) { + proto.options = { + readOnly: { + readTime: options.readOnly.readTime, + }, + }; + } else if (options?.readWrite?.prevTransactionId) { proto.options = { readWrite: { - retryTransaction: transactionId(prevTransaction), + retryTransaction: transactionId(options.readWrite.prevTransactionId), }, }; } const response = { - transaction: transactionId(transaction), + transaction: transactionId(options?.transactionId), }; return { type: 'begin', request: proto, - error, + error: options?.error, response, }; } @@ -278,6 +291,7 @@ function backoff(maxDelay?: boolean): TransactionStep { * Asserts that the given transaction function issues the expected requests. */ function runTransaction<T>( + transactionOptions: ReadWriteTransactionOptions | ReadOnlyTransactionOptions, transactionCallback: ( transaction: Transaction, docRef: DocumentReference @@ -352,7 +366,7 @@ function runTransaction<T>( return await firestore.runTransaction(transaction => { const docRef = firestore.doc('collectionId/documentId'); return transactionCallback(transaction, docRef); - }); + }, transactionOptions); } finally { setTimeoutHandler(setTimeout); expect(expectedRequests.length).to.equal( @@ -366,6 +380,7 @@ function runTransaction<T>( describe('successful transactions', () => { it('empty transaction', () => { return runTransaction( + /* transactionOptions= */ {}, () => { return Promise.resolve(); }, @@ -376,6 +391,7 @@ describe('successful transactions', () => { it('returns value', () => { return runTransaction( + /* transactionOptions= */ {}, () => { return Promise.resolve('bar'); }, @@ -415,19 +431,24 @@ describe('failed transactions', () => { if (retry) { await runTransaction( + /* transactionOptions= */ {}, transactionFunction, - begin('foo1'), + begin({transactionId: 'foo1'}), commit('foo1', undefined, serverError), rollback('foo1'), backoff(), - begin('foo2', 'foo1'), + begin({ + transactionId: 'foo2', + readWrite: {prevTransactionId: 'foo1'}, + }), commit('foo2') ); } else { await expect( runTransaction( + /* transactionOptions= */ {}, transactionFunction, - begin('foo1'), + begin({transactionId: 'foo1'}), commit('foo1', undefined, serverError), rollback('foo1') ) @@ -445,12 +466,13 @@ describe('failed transactions', () => { serverError.code = Status.INVALID_ARGUMENT; await runTransaction( + /* transactionOptions= */ {}, transactionFunction, - begin('foo1'), + begin({transactionId: 'foo1'}), commit('foo1', undefined, serverError), rollback('foo1'), backoff(), - begin('foo2', 'foo1'), + begin({transactionId: 'foo2', readWrite: {prevTransactionId: 'foo1'}}), commit('foo2') ); }); @@ -470,20 +492,25 @@ describe('failed transactions', () => { if (retry) { await runTransaction( + /* transactionOptions= */ {}, transactionFunction, - begin('foo1'), + begin({transactionId: 'foo1'}), query('foo1', serverError), rollback('foo1'), backoff(), - begin('foo2', 'foo1'), + begin({ + transactionId: 'foo2', + readWrite: {prevTransactionId: 'foo1'}, + }), query('foo2'), commit('foo2') ); } else { await expect( runTransaction( + /* transactionOptions= */ {}, transactionFunction, - begin('foo1'), + begin({transactionId: 'foo1'}), query('foo1', serverError), rollback('foo1') ) @@ -506,20 +533,25 @@ describe('failed transactions', () => { if (retry) { await runTransaction( + /* transactionOptions= */ {}, transactionFunction, - begin('foo1'), + begin({transactionId: 'foo1'}), getDocument('foo1', serverError), rollback('foo1'), backoff(), - begin('foo2', 'foo1'), + begin({ + transactionId: 'foo2', + readWrite: {prevTransactionId: 'foo1'}, + }), getDocument('foo2'), commit('foo2') ); } else { await expect( runTransaction( + /* transactionOptions= */ {}, transactionFunction, - begin('foo1'), + begin({transactionId: 'foo1'}), getDocument('foo1', serverError), rollback('foo1') ) @@ -537,20 +569,25 @@ describe('failed transactions', () => { if (retry) { await runTransaction( + /* transactionOptions= */ {}, transactionFunction, - begin('foo1'), + begin({transactionId: 'foo1'}), commit('foo1', /* writes=*/ undefined, serverError), rollback('foo1', serverError), rollback('foo1'), backoff(), - begin('foo2', 'foo1'), + begin({ + transactionId: 'foo2', + readWrite: {prevTransactionId: 'foo1'}, + }), commit('foo2') ); } else { await expect( runTransaction( + /* transactionOptions= */ {}, transactionFunction, - begin('foo1'), + begin({transactionId: 'foo1'}), commit('foo1', /* writes=*/ undefined, serverError), rollback('foo1', serverError) ) @@ -595,7 +632,12 @@ describe('failed transactions', () => { it('requires a promise', () => { return expect( - runTransaction((() => {}) as InvalidApiUsage, begin(), rollback()) + runTransaction( + /* transactionOptions= */ {}, + (() => {}) as InvalidApiUsage, + begin(), + rollback() + ) ).to.eventually.be.rejectedWith( 'You must return a Promise in your transaction()-callback.' ); @@ -618,6 +660,7 @@ describe('failed transactions', () => { it("doesn't retry custom user exceptions in callback", () => { return expect( runTransaction( + /* transactionOptions= */ {}, () => { return Promise.reject('request exception'); }, @@ -633,24 +676,25 @@ describe('failed transactions', () => { return expect( runTransaction( + /* transactionOptions= */ {}, () => Promise.resolve(), - begin('foo1'), + begin({transactionId: 'foo1'}), commit('foo1', [], err), rollback('foo1'), backoff(), - begin('foo2', 'foo1'), + begin({transactionId: 'foo2', readWrite: {prevTransactionId: 'foo1'}}), commit('foo2', [], err), rollback('foo2'), backoff(), - begin('foo3', 'foo2'), + begin({transactionId: 'foo3', readWrite: {prevTransactionId: 'foo2'}}), commit('foo3', [], err), rollback('foo3'), backoff(), - begin('foo4', 'foo3'), + begin({transactionId: 'foo4', readWrite: {prevTransactionId: 'foo3'}}), commit('foo4', [], err), rollback('foo4'), backoff(), - begin('foo5', 'foo4'), + begin({transactionId: 'foo5', readWrite: {prevTransactionId: 'foo4'}}), commit('foo5', [], new Error('Final exception')), rollback('foo5') ) @@ -662,12 +706,13 @@ describe('failed transactions', () => { err.code = Status.RESOURCE_EXHAUSTED; return runTransaction( + /* transactionOptions= */ {}, async () => {}, - begin('foo1'), + begin({transactionId: 'foo1'}), commit('foo1', [], err), rollback('foo1'), backoff(/* maxDelay= */ true), - begin('foo2', 'foo1'), + begin({transactionId: 'foo2', readWrite: {prevTransactionId: 'foo1'}}), commit('foo2') ); }); @@ -676,6 +721,7 @@ describe('failed transactions', () => { describe('transaction operations', () => { it('support get with document ref', () => { return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { return transaction.get(docRef).then(doc => { expect(doc.id).to.equal('documentId'); @@ -689,6 +735,7 @@ describe('transaction operations', () => { it('requires a query or document for get', () => { return runTransaction( + /* transactionOptions= */ {}, (transaction: InvalidApiUsage) => { expect(() => transaction.get()).to.throw( 'Value for argument "refOrQuery" must be a DocumentReference or a Query.' @@ -708,6 +755,7 @@ describe('transaction operations', () => { it('enforce that gets come before writes', () => { return expect( runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { transaction.set(docRef, {foo: 'bar'}); return transaction.get(docRef); @@ -722,6 +770,7 @@ describe('transaction operations', () => { it('support get with query', () => { return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { const query = docRef.parent.where('foo', '==', 'bar'); return transaction.get(query).then(results => { @@ -734,8 +783,32 @@ describe('transaction operations', () => { ); }); + it('supports read-only transactions', () => { + return runTransaction( + {readOnly: true}, + (transaction, docRef) => transaction.get(docRef), + begin({readOnly: {}}), + getDocument(), + commit() + ); + }); + + it('supports read-only transactions with read time', () => { + return runTransaction( + { + readOnly: true, + readTime: Timestamp.fromMillis(1), + }, + (transaction, docRef) => transaction.get(docRef), + begin({readOnly: {readTime: {nanos: 1000000}}}), + getDocument(), + commit() + ); + }); + it('support getAll', () => { return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { const firstDoc = docRef.parent.doc('firstDocument'); const secondDoc = docRef.parent.doc('secondDocument'); @@ -754,6 +827,7 @@ describe('transaction operations', () => { it('support getAll with field mask', () => { return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { const doc = docRef.parent.doc('doc'); @@ -770,6 +844,7 @@ describe('transaction operations', () => { it('enforce that getAll come before writes', () => { return expect( runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { transaction.set(docRef, {foo: 'bar'}); return transaction.getAll(docRef); @@ -794,6 +869,7 @@ describe('transaction operations', () => { }; return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { transaction.create(docRef, {}); return Promise.resolve(); @@ -828,6 +904,7 @@ describe('transaction operations', () => { }; return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { transaction.update(docRef, {'a.b': 'c'}); transaction.update(docRef, 'a.b', 'c'); @@ -852,6 +929,7 @@ describe('transaction operations', () => { }; return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { transaction.set(docRef, {'a.b': 'c'}); return Promise.resolve(); @@ -877,6 +955,7 @@ describe('transaction operations', () => { }; return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { transaction.set(docRef, {'a.b': 'c'}, {merge: true}); return Promise.resolve(); @@ -902,6 +981,7 @@ describe('transaction operations', () => { }; return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { const postRef = docRef.withConverter(postConverterMerge); transaction.set(postRef, {title: 'story'} as Partial<Post>, { @@ -930,6 +1010,7 @@ describe('transaction operations', () => { }; return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { const postRef = docRef.withConverter(postConverter); transaction.set( @@ -952,6 +1033,7 @@ describe('transaction operations', () => { }; return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { transaction.delete(docRef); return Promise.resolve(); @@ -974,6 +1056,7 @@ describe('transaction operations', () => { }; return runTransaction( + /* transactionOptions= */ {}, (transaction, docRef) => { transaction.delete(docRef).set(docRef, {}); return Promise.resolve(); diff --git a/types/firestore.d.ts b/types/firestore.d.ts index ad393532f..558ae812a 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -174,6 +174,28 @@ declare namespace FirebaseFirestore { [key: string]: any; // Accept other properties, such as GRPC settings. } + /** Options to configure a read-only transaction. */ + export interface ReadOnlyTransactionOptions { + /** Set to true to indicate a read-only transaction. */ + readOnly: true; + /** + * If specified, documents are read at the given time. This may not be more + * than 60 seconds in the past from when the request is processed by the + * server. + */ + readTime?: Timestamp; + } + + /** Options to configure a read-write transaction. */ + export interface ReadWriteTransactionOptions { + /** Set to false or omit to indicate a read-write transaction. */ + readOnly?: false; + /** + * The maximum number of attempts for this transaction. Defaults to five. + */ + maxAttempts?: number; + } + /** * `Firestore` represents a Firestore Database and is the entry point for all * Firestore operations. @@ -311,25 +333,30 @@ declare namespace FirebaseFirestore { * modify Firestore documents under lock. You have to perform all reads * before before you perform any write. * - * Documents read during a transaction are locked pessimistically. A - * transaction's lock on a document blocks other transactions, batched - * writes, and other non-transactional writes from changing that document. - * A transaction releases its document locks at commit time or once it times - * out or fails for any reason. + * Transactions can be performed as read-only or read-write transactions. By + * default, transactions are executed in read-write mode. + * + * A read-write transaction obtains a pessimistic lock on all documents that + * are read during the transaction. These locks block other transactions, + * batched writes, and other non-transactional writes from changing that + * document. Any writes in a read-write transactions are committed once + * 'updateFunction' resolves, which also releases all locks. + * + * If a read-write transaction fails with contention, the transaction is + * retried up to five times. The `updateFunction` is invoked once for each + * attempt. * - * Transactions are committed once 'updateFunction' resolves. If a - * transaction fails with contention, the transaction is retried up to five - * times. The `updateFunction` is invoked once for each attempt. + * Read-only transactions do not lock documents. They can be used to read + * documents at a consistent snapshot in time, which may be up to 60 seconds + * in the past. Read-only transactions are not retried. * * Transactions time out after 60 seconds if no documents are read. * Transactions that are not committed within than 270 seconds are also - * aborted. + * aborted. Any remaining locks are released when a transaction times out. * * @param updateFunction The function to execute within the transaction * context. - * @param {object=} transactionOptions Transaction options. - * @param {number=} transactionOptions.maxAttempts The maximum number of - * attempts for this transaction. + * @param transactionOptions Transaction options. * @return If the transaction completed successfully or was explicitly * aborted (by the updateFunction returning a failed Promise), the Promise * returned by the updateFunction will be returned here. Else if the @@ -338,7 +365,9 @@ declare namespace FirebaseFirestore { */ runTransaction<T>( updateFunction: (transaction: Transaction) => Promise<T>, - transactionOptions?: {maxAttempts?: number} + transactionOptions?: + | ReadWriteTransactionOptions + | ReadOnlyTransactionOptions ): Promise<T>; /**