Skip to content

Commit

Permalink
Add TransactionOptions (#6189)
Browse files Browse the repository at this point in the history
* Add TransactionOptions

* Fix whitespace

* Fix whitespace

* Add changeset

* Update comments
  • Loading branch information
tom-andersen committed May 5, 2022
1 parent 5ce0676 commit dfab18a
Show file tree
Hide file tree
Showing 13 changed files with 278 additions and 133 deletions.
5 changes: 5 additions & 0 deletions .changeset/lazy-nails-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/firestore': patch
---

Add `TransactionOptions` param to `runTransaction` method
7 changes: 6 additions & 1 deletion common/api-review/firestore-lite.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export class QuerySnapshot<T = DocumentData> {
export function refEqual<T>(left: DocumentReference<T> | CollectionReference<T>, right: DocumentReference<T> | CollectionReference<T>): boolean;

// @public
export function runTransaction<T>(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise<T>): Promise<T>;
export function runTransaction<T>(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise<T>, options?: TransactionOptions): Promise<T>;

// @public
export function serverTimestamp(): FieldValue;
Expand Down Expand Up @@ -331,6 +331,11 @@ export class Transaction {
update(documentRef: DocumentReference<unknown>, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): this;
}

// @public
export interface TransactionOptions {
readonly maxAttempts?: number;
}

// @public
export type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

Expand Down
7 changes: 6 additions & 1 deletion common/api-review/firestore.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export class QuerySnapshot<T = DocumentData> {
export function refEqual<T>(left: DocumentReference<T> | CollectionReference<T>, right: DocumentReference<T> | CollectionReference<T>): boolean;

// @public
export function runTransaction<T>(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise<T>): Promise<T>;
export function runTransaction<T>(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise<T>, options?: TransactionOptions): Promise<T>;

// @public
export function serverTimestamp(): FieldValue;
Expand Down Expand Up @@ -475,6 +475,11 @@ export class Transaction {
update(documentRef: DocumentReference<unknown>, field: string | FieldPath, value: unknown, ...moreFieldsAndValues: unknown[]): this;
}

// @public
export interface TransactionOptions {
readonly maxAttempts?: number;
}

// @public
export type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

Expand Down
2 changes: 2 additions & 0 deletions packages/firestore/lite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ export {

export { WriteBatch, writeBatch } from '../src/lite-api/write_batch';

export { TransactionOptions } from '../src/lite-api/transaction_options';

export { Transaction, runTransaction } from '../src/lite-api/transaction';

export { setLogLevel, LogLevelString as LogLevel } from '../src/util/log';
Expand Down
2 changes: 2 additions & 0 deletions packages/firestore/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export {

export { Unsubscribe, SnapshotListenOptions } from './api/reference_impl';

export { TransactionOptions } from './api/transaction_options';

export { runTransaction, Transaction } from './api/transaction';

export {
Expand Down
21 changes: 18 additions & 3 deletions packages/firestore/src/api/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@

import { firestoreClientTransaction } from '../core/firestore_client';
import { Transaction as InternalTransaction } from '../core/transaction';
import {
TransactionOptions as TranasactionOptionsInternal,
DEFAULT_TRANSACTION_OPTIONS,
validateTransactionOptions
} from '../core/transaction_options';
import { DocumentReference } from '../lite-api/reference';
import { Transaction as LiteTransaction } from '../lite-api/transaction';
import { validateReference } from '../lite-api/write_batch';
Expand All @@ -25,6 +30,7 @@ import { cast } from '../util/input_validation';
import { ensureFirestoreConfigured, Firestore } from './database';
import { ExpUserDataWriter } from './reference_impl';
import { DocumentSnapshot, SnapshotMetadata } from './snapshot';
import { TransactionOptions } from './transaction_options';

/**
* A reference to a transaction.
Expand Down Expand Up @@ -92,11 +98,20 @@ export class Transaction extends LiteTransaction {
*/
export function runTransaction<T>(
firestore: Firestore,
updateFunction: (transaction: Transaction) => Promise<T>
updateFunction: (transaction: Transaction) => Promise<T>,
options?: TransactionOptions
): Promise<T> {
firestore = cast(firestore, Firestore);
const optionsWithDefaults: TranasactionOptionsInternal = {
...DEFAULT_TRANSACTION_OPTIONS,
...options
};
validateTransactionOptions(optionsWithDefaults);
const client = ensureFirestoreConfigured(firestore);
return firestoreClientTransaction(client, internalTransaction =>
updateFunction(new Transaction(firestore, internalTransaction))
return firestoreClientTransaction(
client,
internalTransaction =>
updateFunction(new Transaction(firestore, internalTransaction)),
optionsWithDefaults
);
}
18 changes: 18 additions & 0 deletions packages/firestore/src/api/transaction_options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license
* Copyright 2022 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.
*/

export { TransactionOptions } from '../lite-api/transaction_options';
5 changes: 4 additions & 1 deletion packages/firestore/src/core/firestore_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
syncEngineWrite
} from './sync_engine_impl';
import { Transaction } from './transaction';
import { TransactionOptions } from './transaction_options';
import { TransactionRunner } from './transaction_runner';
import { View } from './view';
import { ViewSnapshot } from './view_snapshot';
Expand Down Expand Up @@ -483,14 +484,16 @@ export function firestoreClientAddSnapshotsInSyncListener(
*/
export function firestoreClientTransaction<T>(
client: FirestoreClient,
updateFunction: (transaction: Transaction) => Promise<T>
updateFunction: (transaction: Transaction) => Promise<T>,
options: TransactionOptions
): Promise<T> {
const deferred = new Deferred<T>();
client.asyncQueue.enqueueAndForget(async () => {
const datastore = await getDatastore(client);
new TransactionRunner<T>(
client.asyncQueue,
datastore,
options,
updateFunction,
deferred
).run();
Expand Down
39 changes: 39 additions & 0 deletions packages/firestore/src/core/transaction_options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license
* Copyright 2022 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 { Code, FirestoreError } from '../util/error';

export const DEFAULT_TRANSACTION_OPTIONS: TransactionOptions = {
maxAttempts: 5
};

/**
* Options to customize transaction behavior.
*/
export declare interface TransactionOptions {
/** Maximum number of attempts to commit, after which transaction fails. Default is 5. */
readonly maxAttempts: number;
}

export function validateTransactionOptions(options: TransactionOptions): void {
if (options.maxAttempts < 1) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Max attempts must be at least 1'
);
}
}
7 changes: 4 additions & 3 deletions packages/firestore/src/core/transaction_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,24 @@ import { Deferred } from '../util/promise';
import { isNullOrUndefined } from '../util/types';

import { Transaction } from './transaction';

export const DEFAULT_MAX_ATTEMPTS_COUNT = 5;
import { TransactionOptions } from './transaction_options';

/**
* TransactionRunner encapsulates the logic needed to run and retry transactions
* with backoff.
*/
export class TransactionRunner<T> {
private attemptsRemaining = DEFAULT_MAX_ATTEMPTS_COUNT;
private attemptsRemaining: number;
private backoff: ExponentialBackoff;

constructor(
private readonly asyncQueue: AsyncQueue,
private readonly datastore: Datastore,
private readonly options: TransactionOptions,
private readonly updateFunction: (transaction: Transaction) => Promise<T>,
private readonly deferred: Deferred<T>
) {
this.attemptsRemaining = options.maxAttempts;
this.backoff = new ExponentialBackoff(
this.asyncQueue,
TimerId.TransactionRetry
Expand Down
15 changes: 14 additions & 1 deletion packages/firestore/src/lite-api/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
import { getModularInstance } from '@firebase/util';

import { Transaction as InternalTransaction } from '../core/transaction';
import {
DEFAULT_TRANSACTION_OPTIONS,
TransactionOptions as TranasactionOptionsInternal,
validateTransactionOptions
} from '../core/transaction_options';
import { TransactionRunner } from '../core/transaction_runner';
import { fail } from '../util/assert';
import { newAsyncQueue } from '../util/async_queue_impl';
Expand All @@ -39,6 +44,7 @@ import {
LiteUserDataWriter
} from './reference_impl';
import { DocumentSnapshot } from './snapshot';
import { TransactionOptions } from './transaction_options';
import {
newUserDataReader,
parseSetData,
Expand Down Expand Up @@ -266,14 +272,21 @@ export class Transaction {
*/
export function runTransaction<T>(
firestore: Firestore,
updateFunction: (transaction: Transaction) => Promise<T>
updateFunction: (transaction: Transaction) => Promise<T>,
options?: TransactionOptions
): Promise<T> {
firestore = cast(firestore, Firestore);
const datastore = getDatastore(firestore);
const optionsWithDefaults: TranasactionOptionsInternal = {
...DEFAULT_TRANSACTION_OPTIONS,
...options
};
validateTransactionOptions(optionsWithDefaults);
const deferred = new Deferred<T>();
new TransactionRunner<T>(
newAsyncQueue(),
datastore,
optionsWithDefaults,
internalTransaction =>
updateFunction(new Transaction(firestore, internalTransaction)),
deferred
Expand Down
24 changes: 24 additions & 0 deletions packages/firestore/src/lite-api/transaction_options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @license
* Copyright 2022 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.
*/

/**
* Options to customize transaction behavior.
*/
export declare interface TransactionOptions {
/** Maximum number of attempts to commit, after which transaction fails. Default is 5. */
readonly maxAttempts?: number;
}

0 comments on commit dfab18a

Please sign in to comment.