or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

collections-queries.mdcore-database.mddata-snapshots.mddata-types.mddocument-references.mdindex.mdtransactions-batches.md
tile.json

transactions-batches.mddocs/

Transactions and Batches

Types for atomic operations, transactions, and batch writes that ensure data consistency across multiple documents.

Capabilities

Transaction Class

Provides atomic read and write operations that execute as a single unit.

class Transaction {
  private constructor();
  
  /** Read a document within the transaction */
  get<T>(documentRef: DocumentReference<T>): Promise<DocumentSnapshot<T>>;
  
  /** Set document data within the transaction */
  set<T>(
    documentRef: DocumentReference<T>,
    data: Partial<T>,
    options: SetOptions
  ): Transaction;
  set<T>(documentRef: DocumentReference<T>, data: T): Transaction;
  
  /** Update document fields within the transaction */
  update(documentRef: DocumentReference<any>, data: UpdateData): Transaction;
  update(
    documentRef: DocumentReference<any>,
    field: string | FieldPath,
    value: any,
    ...moreFieldsAndValues: any[]
  ): Transaction;
  
  /** Delete a document within the transaction */
  delete(documentRef: DocumentReference<any>): Transaction;
}

WriteBatch Class

Provides batched write operations that execute atomically.

class WriteBatch {
  private constructor();
  
  /** Add a set operation to the batch */
  set<T>(
    documentRef: DocumentReference<T>,
    data: Partial<T>,
    options: SetOptions
  ): WriteBatch;
  set<T>(documentRef: DocumentReference<T>, data: T): WriteBatch;
  
  /** Add an update operation to the batch */
  update(documentRef: DocumentReference<any>, data: UpdateData): WriteBatch;
  update(
    documentRef: DocumentReference<any>,
    field: string | FieldPath,
    value: any,
    ...moreFieldsAndValues: any[]
  ): WriteBatch;
  
  /** Add a delete operation to the batch */
  delete(documentRef: DocumentReference<any>): WriteBatch;
  
  /** Execute all batched operations atomically */
  commit(): Promise<void>;
}

Usage Examples

Transaction Operations

import type { Transaction, DocumentReference, DocumentSnapshot } from "@firebase/firestore-types";

interface BankAccount {
  balance: number;
  accountId: string;
  lastTransaction: Timestamp;
}

interface TransferRequest {
  fromAccount: string;
  toAccount: string;
  amount: number;
}

async function transferMoney(
  firestore: FirebaseFirestore,
  fromAccountRef: DocumentReference<BankAccount>,
  toAccountRef: DocumentReference<BankAccount>,
  amount: number
) {
  try {
    await firestore.runTransaction(async (transaction: Transaction) => {
      // Read both accounts
      const fromDoc = await transaction.get(fromAccountRef);
      const toDoc = await transaction.get(toAccountRef);
      
      if (!fromDoc.exists || !toDoc.exists) {
        throw new Error("One or both accounts do not exist");
      }
      
      const fromData = fromDoc.data();
      const toData = toDoc.data();
      
      if (!fromData || !toData) {
        throw new Error("Failed to read account data");
      }
      
      // Check sufficient funds
      if (fromData.balance < amount) {
        throw new Error("Insufficient funds");
      }
      
      // Perform the transfer
      transaction.update(fromAccountRef, {
        balance: fromData.balance - amount,
        lastTransaction: FieldValue.serverTimestamp()
      });
      
      transaction.update(toAccountRef, {
        balance: toData.balance + amount,
        lastTransaction: FieldValue.serverTimestamp()
      });
    });
    
    console.log("Transfer completed successfully");
  } catch (error) {
    console.error("Transfer failed:", error);
    throw error;
  }
}

Complex Transaction Logic

interface Inventory {
  productId: string;
  stock: number;
  reserved: number;
  lastUpdated: Timestamp;
}

interface Order {
  orderId: string;
  items: Array<{ productId: string; quantity: number }>;
  status: 'pending' | 'confirmed' | 'cancelled';
  createdAt: Timestamp;
}

async function processOrder(
  firestore: FirebaseFirestore,
  orderRef: DocumentReference<Order>,
  inventoryRefs: DocumentReference<Inventory>[],
  orderData: Order
) {
  return firestore.runTransaction(async (transaction: Transaction) => {
    // Read order and all inventory items
    const orderDoc = await transaction.get(orderRef);
    const inventoryDocs = await Promise.all(
      inventoryRefs.map(ref => transaction.get(ref))
    );
    
    // Validate order doesn't exist
    if (orderDoc.exists) {
      throw new Error("Order already exists");
    }
    
    // Check inventory availability
    for (let i = 0; i < orderData.items.length; i++) {
      const item = orderData.items[i];
      const inventoryDoc = inventoryDocs[i];
      
      if (!inventoryDoc.exists) {
        throw new Error(`Product ${item.productId} not found`);
      }
      
      const inventory = inventoryDoc.data();
      if (!inventory) {
        throw new Error(`Failed to read inventory for ${item.productId}`);
      }
      
      const availableStock = inventory.stock - inventory.reserved;
      if (availableStock < item.quantity) {
        throw new Error(`Insufficient stock for ${item.productId}`);
      }
    }
    
    // Create order
    transaction.set(orderRef, {
      ...orderData,
      status: 'confirmed',
      createdAt: FieldValue.serverTimestamp()
    });
    
    // Reserve inventory
    for (let i = 0; i < orderData.items.length; i++) {
      const item = orderData.items[i];
      const inventoryRef = inventoryRefs[i];
      
      transaction.update(inventoryRef, {
        reserved: FieldValue.increment(item.quantity),
        lastUpdated: FieldValue.serverTimestamp()
      });
    }
  });
}

Batch Write Operations

import type { WriteBatch } from "@firebase/firestore-types";

interface User {
  name: string;
  email: string;
  createdAt: Timestamp;
  isActive: boolean;
}

async function bulkCreateUsers(
  firestore: FirebaseFirestore,
  usersCollection: CollectionReference<User>,
  userDataList: Omit<User, 'createdAt'>[]
) {
  // Firestore batches are limited to 500 operations
  const batchSize = 500;
  const batches: WriteBatch[] = [];
  
  for (let i = 0; i < userDataList.length; i += batchSize) {
    const batch = firestore.batch();
    const batchData = userDataList.slice(i, i + batchSize);
    
    batchData.forEach(userData => {
      const docRef = usersCollection.doc(); // Auto-generate ID
      batch.set(docRef, {
        ...userData,
        createdAt: FieldValue.serverTimestamp()
      });
    });
    
    batches.push(batch);
  }
  
  // Execute all batches
  await Promise.all(batches.map(batch => batch.commit()));
  console.log(`Created ${userDataList.length} users in ${batches.length} batches`);
}

Mixed Batch Operations

interface Product {
  name: string;
  price: number;
  category: string;
  inStock: boolean;
}

interface Category {
  name: string;
  productCount: number;
  lastUpdated: Timestamp;
}

async function updateProductsAndCategories(
  firestore: FirebaseFirestore,
  updates: {
    productsToUpdate: Array<{ ref: DocumentReference<Product>; data: Partial<Product> }>;
    productsToDelete: DocumentReference<Product>[];
    categoryUpdates: Array<{ ref: DocumentReference<Category>; countChange: number }>;
  }
) {
  const batch = firestore.batch();
  
  // Update products
  updates.productsToUpdate.forEach(({ ref, data }) => {
    batch.update(ref, {
      ...data,
      lastUpdated: FieldValue.serverTimestamp()
    });
  });
  
  // Delete products
  updates.productsToDelete.forEach(ref => {
    batch.delete(ref);
  });
  
  // Update category counts
  updates.categoryUpdates.forEach(({ ref, countChange }) => {
    batch.update(ref, {
      productCount: FieldValue.increment(countChange),
      lastUpdated: FieldValue.serverTimestamp()
    });
  });
  
  // Execute all operations atomically
  await batch.commit();
  console.log("Batch update completed successfully");
}

Error Handling and Retry Logic

async function robustTransactionOperation<T>(
  firestore: FirebaseFirestore,
  operation: (transaction: Transaction) => Promise<T>,
  maxRetries: number = 3
): Promise<T> {
  let lastError: Error;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await firestore.runTransaction(operation);
    } catch (error) {
      lastError = error as Error;
      
      // Don't retry certain errors
      if (error instanceof Error) {
        const firestoreError = error as any;
        if (firestoreError.code === 'permission-denied' || 
            firestoreError.code === 'not-found') {
          throw error;
        }
      }
      
      console.log(`Transaction attempt ${attempt} failed:`, error);
      
      if (attempt === maxRetries) {
        break;
      }
      
      // Exponential backoff
      const delay = Math.pow(2, attempt - 1) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

// Usage
async function safeTransfer(
  firestore: FirebaseFirestore,
  fromAccountRef: DocumentReference<BankAccount>,
  toAccountRef: DocumentReference<BankAccount>,
  amount: number
) {
  return robustTransactionOperation(firestore, async (transaction) => {
    // Transaction logic here (same as transferMoney example)
    const fromDoc = await transaction.get(fromAccountRef);
    const toDoc = await transaction.get(toAccountRef);
    
    // ... rest of transaction logic
  });
}

Performance Considerations

// Efficient batch sizing
async function efficientBatchWrite<T>(
  firestore: FirebaseFirestore,
  operations: Array<{
    type: 'set' | 'update' | 'delete';
    ref: DocumentReference<T>;
    data?: any;
  }>
) {
  const maxBatchSize = 500; // Firestore limit
  const batches: WriteBatch[] = [];
  
  for (let i = 0; i < operations.length; i += maxBatchSize) {
    const batch = firestore.batch();
    const batchOps = operations.slice(i, i + maxBatchSize);
    
    batchOps.forEach(op => {
      switch (op.type) {
        case 'set':
          batch.set(op.ref, op.data);
          break;
        case 'update':
          batch.update(op.ref, op.data);
          break;
        case 'delete':
          batch.delete(op.ref);
          break;
      }
    });
    
    batches.push(batch);
  }
  
  // Execute batches in parallel (be careful with rate limits)
  const results = await Promise.allSettled(
    batches.map(batch => batch.commit())
  );
  
  const failed = results.filter(result => result.status === 'rejected');
  if (failed.length > 0) {
    console.error(`${failed.length} batches failed`);
    throw new Error('Some batch operations failed');
  }
  
  console.log(`Successfully executed ${batches.length} batches`);
}