> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ditto.live/llms.txt
> Use this file to discover all available pages before exploring further.

# Transactions

> Transactions are a way to group multiple operations into a single database commit.

<Info>
  This feature is available in v4.11 and above.
</Info>

## Introduction

DQL transactions allow developers to execute multiple DQL statements in a single
atomic operation. Using transactions ensures either all statements are executed
successfully or none are executed at all, and provides consistency guarantees so
that consecutive statements within a transaction operate on the same snapshot of
the data. DQL read-write transactions are executed at serializable isolation
level, which makes them suitable for use cases that require strong consistency
guarantees.

## Usage

Use the `transaction()` method on `DittoStore` to run a given block in a
read-write transaction:

<CodeGroup>
  ```swift Swift theme={null}
  let completionAction = try await ditto.store.transaction(hint: "transaction example") { transaction in
      let queryResult1 = try await transaction.execute(/* query 1 */)
      let queryResult2 = try await transaction.execute(/* query 2 */)
      // ...
      let queryResultN = try await transaction.execute(/* query N */)

      // Complete by returning a `TransactionCompletionAction`
      return .commit // Or .rollback
  }
  ```

  ```kotlin Kotlin theme={null}
  var completionAction = ditto.store.transaction(hint = "transaction example") { transaction ->
      transaction.execute(/* query 1 */).use { queryResult1 -> }
      transaction.execute(/* query 2 */).use { queryResult2 -> }
      // ...
      transaction.execute(/* query N */).use { queryResultN -> }

      // Complete by returning a `DittoTransactionCompletionAction`
      DittoTransactionCompletionAction.Commit // Or DittoTransactionCompletionAction.Rollback
  };
  ```

  ```js JS theme={null}
  const completionAction = await ditto.store.transaction(async (transaction) => {
      const queryResult1 = await transaction.execute(/* query 1 */);
      const queryResult2 = await transaction.execute(/* query 2 */);
      // ...
      const queryResultN = await transaction.execute(/* query N */);

      // Complete by returning a `TransactionCompletionAction`
      return "commit"; // Or "rollback"
  }, { hint: "transaction example" });
  ```

  ```typescript TS theme={null}
  import { Ditto, Transaction, QueryResult, TransactionCompletionAction } from '@dittolive/ditto'

  const completionAction: TransactionCompletionAction = await ditto.store.transaction(
    async (transaction: Transaction): Promise<TransactionCompletionAction> => {
      const queryResult1: QueryResult = await transaction.execute(/* query 1 */)
      const queryResult2: QueryResult = await transaction.execute(/* query 2 */)
      // ...
      const queryResultN: QueryResult = await transaction.execute(/* query N */)

      // Complete by returning a `TransactionCompletionAction`
      return "commit" // Or "rollback"
    },
    { hint: "transaction example" }
  )
  ```

  ```java Java theme={null}
  var completionAction = ditto.getStore().transaction("transaction example", transaction -> {
      try
      {
        try (var queryResult1 = transaction.execute(/* query 1 */)) { }
        try (var queryResult2 = transaction.execute(/* query 2 */)) { }
        // ...
        try (var queryResultN = transaction.execute(/* query N */)) { }

        // Complete by returning a `DittoTransactionCompletionAction`
        return DittoTransactionCompletionAction.Commit; // or DittoTransactionCompletionAction.Rollback
      } catch (DittoError e) {
          throw new RuntimeException(e);
      }
  });
  ```

  ```csharp C# theme={null}
  var completionAction = await ditto.Store.TransactionAsync(async transaction =>
  {
      using var queryResult1 = await transaction.ExecuteAsync(/* query 1 */);
      using var queryResult2 = await transaction.ExecuteAsync(/* query 2 */);
      // ...
      using var queryResultN = await transaction.ExecuteAsync(/* query N */);

      // Complete by returning a `DittoTransactionCompletionAction`
      return DittoTransactionCompletionAction.Commit; // Or DittoTransactionCompletionAction.Rollback
  }, hint: "transaction example");
  ```

  ```cpp C++ theme={null}
  const auto completion_action = ditto.get_store().transaction(
    TransactionOptions().set_hint("transaction example"),
    [&](Transaction &transaction) -> TransactionCompletionAction {
      const auto query_result_1 = transaction.execute(/* query 1 */);
      const auto query_result_2 = transaction.execute(/* query 2 */);
      // ...
      const auto query_result_n = transaction.execute(/* query N */);

      // Complete by returning a `TransactionCompletionAction`
      return TransactionCompletionAction::commit; // Or TransactionCompletionAction::rollback
    }
  );
  ```

  ```rust Rust theme={null}
  let mut options = CreateTransactionOptions::default();
  options.hint = Some("transaction example");

  let completion_action = ditto.store().transaction_with_options::<_, DittoError>(
    options,
    async |transaction| {
      let query_result_1 = transaction.execute(/* query 1 */).await?;
      let query_result_2 = transaction.execute(/* query 2 */).await?;
      // ...
      let query_result_n = transaction.execute(/* query N */).await?;

      Ok(TransactionCompletionAction::Commit)
    },
  ).await?;
  ```

  ```dart Dart theme={null}
  final completionAction = await ditto.store.transaction(
    hint: "transaction example",
    (transaction) async {
      final queryResult1 = await transaction.execute(/* query 1 */);
      final queryResult2 = await transaction.execute(/* query 2 */);
      // ...
      final queryResultN = await transaction.execute(/* query N */);

      // Complete by returning a `TransactionCompletionAction`
      return TransactionCompletionAction.commit; // Or TransactionCompletionAction.rollback
    },
  );
  ```

  ```go Go theme={null}
  completionAction, err := dit.Store().Transaction(
      nil, // default options
      func(tx *ditto.Transaction) (ditto.TransactionCompletionAction, error) {
          queryResult1, err := transaction.Execute(/* query 1 */)
          if (err != nil) {
              return ditto.TransactionCompletionActionRollback, err
          }
          defer queryResult1.Close()  // cleanup

          queryResult2, err := transaction.Execute(/* query 2 */)
          if (err != nil) {
              return ditto.TransactionCompletionActionRollback, err
          }
          defer queryResult2.Close()  // cleanup

          // ...
          queryResultN, err := transaction.Execute(/* query N */)
          if (err != nil) {
              return ditto.TransactionCompletionActionRollback, err
          }
          defer queryResultN.Close()  // cleanup

          // Complete by returning a `TransactionCompletionAction`
          return ditto.TransactionCompletionActionCommit, nil
      }
  )
  if err != nil {
      return err
  }
  ```
</CodeGroup>

The transaction is committed if the block returns `.commit` and rolled back if
it returns `.rollback`. The transaction will also be implicitly rolled back if
an error is thrown at any point inside the block.

You can alternatively return a value from a transaction using the second variant
of the method. Returning any value will commit the transaction and throwing an
error will roll it back:

<CodeGroup>
  ```swift Swift theme={null}
  let result: DittoQueryResult? = try await ditto.store.transaction(hint: "transaction returning a value") { transaction in
      let queryResult1 = try await transaction.execute(/* query 1 */)
      let queryResult2 = try await transaction.execute(/* query 2 */)
      // ...
      let queryResultN = try await transaction.execute(/* query N */)
      return queryResultN
  }
  ```

  ```kotlin Kotlin theme={null}
  ditto.store.transaction<DittoQueryResult>(hint = "transaction returning a value") { transaction ->
      transaction.execute(/* query 1 */).use { queryResult1 -> }
      transaction.execute(/* query 2 */).use { queryResult2 -> }
      // ...
      transaction.execute(/* query N */)
  }.use { queryResultN ->
      // ...
  }
  ```

  ```js JS theme={null}
  const result: QueryResult = await ditto.store.transaction(async (transaction) => {
    const queryResult1 = await transaction.execute(/* query 1 */);
    const queryResult2 = await transaction.execute(/* query 2 */);
    // ...
    const queryResultN = await transaction.execute(/* query N */);
    return queryResultN;
  }, { hint: "transaction returning a value" });
  ```

  ```typescript TS theme={null}
  import { Ditto, Transaction, QueryResult } from '@dittolive/ditto'

  const result: QueryResult = await ditto.store.transaction(
    async (transaction: Transaction): Promise<QueryResult> => {
      const queryResult1: QueryResult = await transaction.execute(/* query 1 */)
      const queryResult2: QueryResult = await transaction.execute(/* query 2 */)
      // ...
      const queryResultN: QueryResult = await transaction.execute(/* query N */)
      return queryResultN
    },
    { hint: "transaction returning a value" }
  )
  ```

  ```java Java theme={null}
  try (var queryResultN = ditto.getStore().transactionReturning("transaction returning a value", transaction -> {
      try {
          try (var queryResult1 = transaction.execute(/* query 1 */)) { }
          try (var queryResult2 = transaction.execute(/* query 2 */)) { }
          // ...
          return transaction.execute(/* query N */);
      } catch (DittoError e) {
          throw new RuntimeException(e);
      }
  })) {
    // ...
  }
  ```

  ```csharp C# theme={null}
  using var queryResultN = await ditto.Store.TransactionAsync(async transaction =>
  {
      using var queryResult1 = await transaction.ExecuteAsync(/* query 1 */);
      using var queryResult2 = await transaction.ExecuteAsync(/* query 2 */);
      // ...
      return await transaction.ExecuteAsync(/* query N */);
  }, hint: "transaction returning a value");
  ```

  ```cpp C++ theme={null}
  const auto query_result = await ditto.get_store().transaction_returning<QueryResult>(
    TransactionOptions().set_hint("transaction returning a value"),
    [&](Transaction &transaction) -> QueryResult {
      const auto query_result_1 = transaction.execute(/* query 1 */);
      const auto query_result_2 = transaction.execute(/* query 2 */);
      // ...
      const auto query_result_n = transaction.execute(/* query N */);
      return query_result_n;
    }
  );
  ```

  ```rust Rust theme={null}
  let mut options = CreateTransactionOptions::default();
  options.hint = Some("transaction returning a value");

  let query_result = ditto.store().transaction_with_options::<_, DittoError>(
    options,
    async |txn| {
      let query_result_1 = txn.execute(/* query 1 */).await?;
      let query_result_2 = txn.execute(/* query 2 */).await?;
      // ...
      let query_result_n = txn.execute(/* query N */).await?;
      Ok(query_result_n)
    },
  ).await?;
  ```

  ```dart Dart theme={null}
  final queryResult = await ditto.store.transaction(
    hint: "transaction returning a value",
    (transaction) async {
      final queryResult1 = await transaction.execute(/* query 1 */);
      final queryResult2 = await transaction.execute(/* query 2 */);
      // ...
      final queryResultN = await transaction.execute(/* query N */);
      return queryResultN;
    },
  );
  ```

  ```go Go theme={null}
  queryResult, err := ditto.TransactionWithResult(
      dit.Store(),
      ditto.TransactionOptions{
          Hint: "transaction returning a value",
      },
      func(transaction *ditto.Transaction) (*QueryResult, error) {
          queryResult1, err := transaction.Execute(/* query 1 */)
          if (err != nil) {
              return err
          }
          defer queryResult1.Close()  // cleanup

          queryResult2, _ := transaction.Execute(/* query 2 */)
          if (err != nil) {
              return err
          }
          defer queryResult2.Close()  // cleanup

          // ...

          queryResultN, err := transaction.Execute(/* query N */)
          return queryResultN, err
      }
  )
  if err != nil {
      return err
  }
  defer queryResult.Close()  // cleanup
  ```
</CodeGroup>

### Concurrency and Read-Only Transactions

Transactions are initiated as read-write transactions by default, so only a
single read-write transaction is being executed at any given time. Any other
read-write transaction started concurrently will wait until the current
transaction has been committed or rolled back. Therefore, it is crucial to make
sure a transaction finishes as early as possible so other read-write
transactions aren't blocked for a long time. We recommend setting the `hint`
parameter for transactions, which improves the readability of log output related
to transactions and is especially helpful for debugging conflicts between
multiple transactions. Also see section [Deadlock
Prevention](#deadlock-prevention) for more information.

A transaction can be configured to be read-only using the `options` parameter.
Multiple read-only transactions can be executed concurrently, including
concurrently with a read-write transaction. However, executing a mutating DQL
statement in a read-only transaction will throw an error.

<CodeGroup>
  ```swift Swift theme={null}
  // Read-only transaction with hint:
  try await ditto.store.transaction(hint: "Read-only transaction", isReadOnly: true) { transaction in
      // ...
      return .commit
  }
  ```

  ```kotlin Kotlin theme={null}
  // Read-only transaction with hint:
  ditto.store.transaction(hint = "Read-only transaction", isReadonly = true) { transaction ->
      // ...
      DittoTransactionCompletionAction.Commit
  }
  ```

  ```js JS theme={null}
  // Read-only transaction with hint:
  await ditto.store.transaction(async (transaction) => {
    // ...
    return "commit";
  }, { hint: "Read-only transaction", isReadOnly: true });
  ```

  ```typescript TS theme={null}
  import { Ditto, Transaction, TransactionCompletionAction } from '@dittolive/ditto'

  // Read-only transaction with hint:
  await ditto.store.transaction(
    async (transaction: Transaction): Promise<TransactionCompletionAction> => {
      // ...
      return "commit"
    },
    { hint: "Read-only transaction", isReadOnly: true }
  )
  ```

  ```java Java theme={null}
  // Read-only transaction with hint:
  ditto.getStore().transaction("Read-only transaction", true, transaction -> {
      // ...
      return DittoTransactionCompletionAction.Commit;
  });
  ```

  ```csharp C# theme={null}
  // Read-only transaction with hint:
  await ditto.Store.TransactionAsync(async transaction =>
  {
      // ...
      return DittoTransactionCompletionAction.Commit;
  }, hint: "Read-only transaction", isReadOnly: true);
  ```

  ```cpp C++ theme={null}
  ditto.get_store().transaction(
    TransactionOptions().set_hint("transaction example").set_read_only(true),
    [&](Transaction &transaction) -> TransactionCompletionAction {
      // ...
      return TransactionCompletionAction::commit;
    }
  );
  ```

  ```rust Rust theme={null}
  let mut options = CreateTransactionOptions::default();
  options.is_read_only = true;
  options.hint = Some("Read-only transaction");

  let query_result = ditto.store().transaction_with_options::<_, DittoError>(
    options,
    async |txn| {
      // ...
      Ok(TransactionCompletionAction::Commit)
    },
  ).await?;
  ```

  ```dart Dart theme={null}
  // Read-only transaction with hint:
  final completionAction = await ditto.store.transaction(
    hint: "Read-only transaction",
    isReadOnly: true,
    (transaction) async {
      // ...
      return TransactionCompletionAction.commit;
    },
  );
  ```

  ```go Go theme={null}
  completionAction, err := dit.Store().Transaction(
      ditto.TransactionOptions{
          Hint:       "Read-only transaction,
          IsReadOnly: true,
      },
      func(transaction *ditto.Transaction) (ditto.TransactionCompletionAction, error) {
          // ...
          return ditto.TransactionCompletionActionCommit, nil
      }
  )
  if err != nil {
      return err
  }
  ```
</CodeGroup>

### Transaction Info

The closure passed to `transaction()` receives a `DittoTransaction` object. In
addition to the `execute()` method, it also provides an `info` property that
returns a `DittoTransactionInfo` object. This object contains information about
the transaction, such as the transaction's unique ID, the transaction's hint,
and the transaction's options.

<CodeGroup>
  ```swift Swift theme={null}
  try await ditto.store.transaction { transaction in
      let id = transaction.info.id
      let hint = transaction.info.hint
      let isReadOnly = transaction.info.isReadOnly
      // ...
      return .commit
  }
  ```

  ```kotlin Kotlin theme={null}
  ditto.store.transaction() { transaction ->
      val id = transaction.info.id
      val hint = transaction.info.hint
      val isReadOnly = transaction.info.isReadOnly
      // ...
      DittoTransactionCompletionAction.Commit
  }
  ```

  ```js JS theme={null}
  await ditto.store.transaction(async (transaction) => {
    const id = transaction.info.id;
    const hint = transaction.info.hint;
    const isReadOnly = transaction.info.isReadOnly;
    // ...
    return "commit";
  });
  ```

  ```typescript TS theme={null}
  import { Ditto, Transaction, TransactionInfo, TransactionCompletionAction } from '@dittolive/ditto'

  await ditto.store.transaction(
    async (transaction: Transaction): Promise<TransactionCompletionAction> => {
      const info: TransactionInfo = transaction.info
      const id: string = info.id
      const hint: string | undefined = info.hint
      const isReadOnly: boolean = info.isReadOnly
      // ...
      return "commit"
    }
  )
  ```

  ```java Java theme={null}
  ditto.getStore().transaction(transaction -> {
      var id = transaction.getInfo().getId();
      var hint = transaction.getInfo().getHint();
      var isReadOnly = transaction.getInfo().isReadOnly();
      // ...
      return DittoTransactionCompletionAction.Commit;
  });
  ```

  ```csharp C# theme={null}
  await ditto.Store.TransactionAsync(async transaction =>
  {
      var id = transaction.Info.Id;
      var hint = transaction.Info.Hint;
      var isReadOnly = transaction.Info.IsReadOnly;
      // ...
      return DittoTransactionCompletionAction.Commit;
  });
  ```

  ```cpp C++ theme={null}
  ditto.get_store().transaction(
    [&](Transaction &transaction) -> TransactionCompletionAction {
      const auto info = transaction.get_info();
      const auto id = info.id;
      const auto hint = info.hint;
      const auto is_read_only = info.is_read_only;
      // ...
      return TransactionCompletionAction::commit;
    });
  ```

  ```rust Rust theme={null}
  ditto.store().transaction::<_, DittoError>(async |transaction| {
    let info = transaction.info();
    let id = info.id;
    let hint = info.hint;
    let is_read_only = info.is_read_only;
    // ...
    Ok(TransactionCompletionAction::Commit)
  }).await?;
  ```

  ```dart Dart theme={null}
  await ditto.store.transaction(
    (transaction) async {
      final id = transaction.info.id;
      final hint = transaction.info.hint;
      final isReadOnly = transaction.info.isReadOnly;
      // ...
      return TransactionCompletionAction.commit;
    },
  );
  ```

  ```go Go theme={null}
  completionAction, err := dit.Store().Transaction(
      nil, // default options
      func(transaction *ditto.Transaction) (ditto.TransactionCompletionAction, error) {
          info := transaction.Info()
          id := info.ID
          hint := info.Hint
          isReadOnly := info.IsReadOnly
          // ...
          return ditto.TransactionCompletionActionCommit, nil
      }
  )
  if err != nil {
      return err
  }
  ```
</CodeGroup>

### Closing Ditto with Pending Transactions

Ditto waits for all pending transactions to complete before closing. Ditto is
closing when the `close()` method is called, when the instance is garbage
collected, or when it goes out of scope.

<Warning>
  The Flutter SDK does not support waiting for pending transactions to complete. Make sure to await all transactions' completion before closing the Ditto instance.
</Warning>

### Nested Non-Transaction Operations

Be aware that Ditto does not prevent running operations inside a transaction
block without using the transaction object. This can lead to unexpected behavior
and deadlocks and must be avoided.

<CodeGroup>
  ```swift Swift theme={null}
  try await ditto.store.transaction { transaction in
      try await transaction.execute(/* query 1 */) // ✅ OK
      try await ditto.store.execute(/* query 2 */) // ⚠️ Avoid this
      return .commit
  }
  ```

  ```kotlin Kotlin theme={null}
  ditto.store.transaction() { transaction ->
      transaction.execute(/* query 1 */).use { } // ✅ OK
      ditto.store.execute(/* query 2 */) // ⚠️ Avoid this

      DittoTransactionCompletionAction.Commit
  }
  ```

  ```js JS theme={null}
  await ditto.store.transaction(async (transaction) => {
    await transaction.execute(/* query 1 */); // ✅ OK
    await ditto.store.execute(/* query 2 */); // ⚠️ Avoid this
    return "commit";
  });
  ```

  ```typescript TS theme={null}
  import { Ditto, Transaction, TransactionCompletionAction } from '@dittolive/ditto'

  await ditto.store.transaction(
    async (transaction: Transaction): Promise<TransactionCompletionAction> => {
      await transaction.execute(/* query 1 */) // ✅ OK
      await ditto.store.execute(/* query 2 */) // ⚠️ Avoid this
      return "commit"
    }
  )
  ```

  ```java Java theme={null}
  ditto.getStore().transaction(transaction -> {
      try
      {
          try (var result = transaction.execute(/* query 1 */)) { } // ✅ OK
          ditto.getStore().execute(/* query 2 */); // ⚠️ Avoid this

          return DittoTransactionCompletionAction.Commit;
      } catch (DittoError e) {
          throw new RuntimeException(e);
      }
  });
  ```

  ```csharp C# theme={null}
  await ditto.Store.TransactionAsync(async transaction =>
  {
      using var result = await transaction.ExecuteAsync(/* query 1 */); // ✅ OK
      await ditto.Store.ExecuteAsync(/* query 2 */); // ⚠️ Avoid this

      return DittoTransactionCompletionAction.Commit;
  });
  ```

  ```cpp C++ theme={null}
  ditto.get_store().transaction(
    [&](Transaction &transaction) -> TransactionCompletionAction {
      transaction.execute(/* query 1 */);       // ✅ OK
      ditto.get_store().execute(/* query 2 */); // ⚠️ Avoid this
      return TransactionCompletionAction::commit;
    }
  );
  ```

  ```rust Rust theme={null}
  ditto.store().transaction::<_, DittoError>(async |transaction| {
    transaction.execute(/* query 1 */).await?;   // ✅ OK
    ditto.store().execute(/* query 1 */).await?; // ⚠️ Avoid this
    Ok(TransactionCompletionAction::Commit)
  }).await?;
  ```

  ```dart Dart theme={null}
  // The Flutter SDK will throw an error when trying to run a
  // non-transaction operation inside a transaction block
  await ditto.store.transaction(
    (transaction) async {
      await transaction.execute(/* query 1 */); // ✅ OK
      await ditto.store.execute(/* query 2 */); // ⚠️ Will throw an error
      return TransactionCompletionAction.commit;
    },
  );
  ```

  ```go Go theme={null}
  completionAction, err := dit.Store().Transaction(
      nil, // default options
      func(transaction *ditto.Transaction) (ditto.TransactionCompletionAction, error) {
          result1, err := transaction.Execute(/* query 1 */);  // ✅ OK
          if err != nil {
              return nil, err
          }
          defer result1.Close()  // cleanup

          result2, err := dit.Store().Execute(/* query 2 */); // ⚠️ Avoid this
          if err != nil {
              return nil, err
          }
          defer result2.Close()

          return ditto.TransactionCompletionActionCommit, nil
      }
  )
  if err != nil {
      return err
  }
  ```
</CodeGroup>

### Deadlock Prevention

Read-write transactions should only be used when necessary, as they can lead to
deadlocks if not implemented correctly. This section provides some information
on how to avoid and debug deadlocks and other performance issues when using
read-write transactions. Note that the following does not apply to read-only
transactions.

When a read-write transaction is started, a lock on the Ditto store is acquired
and held until the transaction is committed or rolled back. Any attempt to start
a new read-write transaction concurrently will wait until the current
transaction is completed. It is important to avoid long-running operations
inside a transaction block, as these prevent other transactions from starting
which can lead to performance issues.

The transaction's lock can also lead to deadlocks if the current transaction is
waiting for the new transaction to complete. In particular, a read-write
transaction must *never* be initiated from within another read-write transaction
block. In this case the inner transaction will not start until the outer
transaction is completed and the outer transaction will never complete because
it is waiting for the inner transaction.

This caveat also applies to executing mutating DQL statements in a transaction
block without using the transaction object. As each mutating DQL statement is
run in its own transaction, this can lead to a deadlock. See the previous
section [Nested Non-Transaction
Operations](#nested-non-transaction-operations) for an example.

Ditto keeps track of how long a transaction has been running and will start
logging warnings annotated with the transaction's `hint` every 5 seconds if a
transaction runs for more than 10 seconds. These time intervals can be
configured using the following [system parameters](/sdk/latest/sync/using-alter-system):

* `transaction_duration_before_logging_ms`: The amount of time in milliseconds
  that a transaction can be running for before logging should begin.
* `transaction_trace_interval_ms`: The interval of time in milliseconds between
  progressive trace statements

These warnings will be emitted with a progressively escalating log level,
starting at `Debug`.

### Replication of Changes from Transactions

In a Ditto mesh where peers share identical subscriptions, transaction atomicity
and causal consistency is ensured by the replication process. However, if a peer
subscribes to only a subset of the changes within a transaction, that peer will
receive only the subscribed subset. This can lead to unexpected consequences if
the peer later broadens its subscriptions or replicates these partial changes to
other peers with broader subscriptions.

For the first scenario, consider a mesh with two peers. If Peer 1 modifies
documents A and B, but Peer 2 subscribes only to changes on document A, then
Peer 2 will receive only the changes to document A. If Peer 2 subsequently
expands its subscriptions to include document B, it will not immediately have
access to changes to B from the transaction; these changes will only become
available once replication catches up with the extended subscription set.

A similar situation arises when replication occurs in stages across peers with
varying subscription scopes. Suppose Peer 1 modifies both A and B, and Peer 2
subscribes only to document A. Peer 2 receives only changes to A. If Peer 3 is
connected only to Peer 2 but subscribes to both A and B, it will still receive
only the changes to A from the transaction since Peer 2 did not have access to
changes on B. This can lead to unexpected behavior because Peer 3's
subscriptions includes both documents, but changes to B are missing due to Peer
2's limited subscriptions. This issue can be mitigated by ensuring sufficient
connectivity with peers that have broader subscription sets.

### Error Handling

When an error is thrown at any point inside the transaction block or while
committing the transaction, the transaction is implicitly rolled back and the
error is propagated to the caller of `transaction()`.

If errors occur in an `execute()` call within a transaction block and the error
is caught and handled within the block, the transaction will continue to run and
not be rolled back.

When a transaction is configured to be read-only, the execution of mutating DQL
statements (`UPDATE`, `DELETE`, etc.) will throw an error. Unless this error is
caught, it will be propagated to the caller of `transaction(:)` and the
transaction will be rolled back.

## Limitations

Concurrent read-write transactions are not supported. If a read-write
transaction is already in progress, any attempt to start a new transaction will
wait until the current transaction is completed. See section [Deadlock
Prevention](#deadlock-prevention) for more information. Transaction atomicity
may be temporarily compromised if a mesh is set up such that changes are
replicated to a peer with subscriptions covering only a portion of a
transaction's changes, and that peer then replicates to other peers with broader
subscriptions or full coverage of the transaction. Additionally, if a peer
initially subscribes to only part of a transaction's changes and later extends
its subscriptions, it won't immediately receive the remaining changes from the
original transaction. These additional changes will only become available once
replication aligns with the new subscription scope. For more details, refer to
[Replication of Changes from
Transactions](#replication-of-changes-from-transactions) section.
