/* * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this library 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 NativeError from '@react-native-firebase/app/dist/module/internal/NativeFirebaseError'; import Transaction, { type TransactionMeta } from './FirestoreTransaction'; import type { FirestoreInternal } from './types/internal'; let transactionId = 0; const generateTransactionId = (): number => transactionId++; function validateMaxAttempts(options?: { maxAttempts?: number }): number { const maxAttempts = options?.maxAttempts; if (maxAttempts === undefined) { return 0; } if (maxAttempts < 1) { throw new Error('Max attempts must be at least 1'); } return maxAttempts; } export interface TransactionPendingEntry { meta: TransactionMeta; transaction: Transaction; } export interface TransactionEventBody { type?: 'update' | 'error' | 'complete'; error?: unknown; } export interface TransactionEvent { body: TransactionEventBody; listenerId?: number; } export default class FirestoreTransactionHandler { _firestore: FirestoreInternal; _pending: Record; constructor(firestore: FirestoreInternal) { this._firestore = firestore; this._pending = {}; this._firestore.emitter.addListener( this._firestore.eventNameForApp('firestore_transaction_event'), this._onTransactionEvent.bind(this), ); } _onTransactionEvent(event: TransactionEvent): void { switch (event.body.type) { case 'update': this._handleUpdate(event); break; case 'error': this._handleError(event); break; case 'complete': this._handleComplete(event); break; } } async _handleUpdate(event: TransactionEvent): Promise { const id = event.listenerId; if (id === undefined || !this._pending[id]) { if (id !== undefined) { this._remove(id); } return; } const { meta, transaction } = this._pending[id]; const { updateFunction } = meta; transaction._prepare(); let finalError: unknown; let updateFailed = false; let pendingResult: unknown; try { const possiblePromise = updateFunction(transaction); if (!possiblePromise || typeof (possiblePromise as Promise)?.then !== 'function') { throw new Error( "firebase.firestore().runTransaction(*) 'updateFunction' must return a Promise.", ); } pendingResult = await possiblePromise; } catch (exception) { updateFailed = true; finalError = exception; } const pendingAfter = this._pending[id]; if (!pendingAfter) { return; } if (updateFailed || finalError) { pendingAfter.meta.reject?.(finalError); return; } pendingAfter.transaction._pendingResult = pendingResult; return this._firestore.native.transactionApplyBuffer( id, pendingAfter.transaction._commandBuffer, ); } _handleError(event: TransactionEvent): void { const id = event.listenerId; const { error } = event.body; if (id === undefined || !this._pending[id]) { return; } const { meta } = this._pending[id]; if (meta && error) { const errorAndStack = NativeError.fromEvent(error, 'firestore', meta.stack); meta.reject?.(errorAndStack); } } _handleComplete(event: TransactionEvent): void { const id = event.listenerId; if (id === undefined || !this._pending[id]) { return; } const { meta, transaction } = this._pending[id]; if (meta) { meta.resolve?.(transaction._pendingResult); } } _add( updateFunction: (transaction: Transaction) => Promise, options?: { maxAttempts?: number }, ): Promise { const maxAttempts = validateMaxAttempts(options); const id = generateTransactionId(); const meta: TransactionMeta = { id, updateFunction, stack: new Error().stack?.split('\n').slice(2).join('\n'), }; this._pending[id] = { meta, transaction: new Transaction(this._firestore, meta), }; return new Promise((resolve, reject) => { this._firestore.native.transactionBegin(id, maxAttempts); meta.resolve = (result: unknown) => { this._remove(id); resolve(result); }; meta.reject = (error: unknown) => { this._remove(id); reject(error); }; }); } _remove(id: number): void { this._firestore.native.transactionDispose(id); delete this._pending[id]; } }