# Transaction API Reference

## createTransaction

```ts
import { createTransaction } from "@tanstack/db"

const tx = createTransaction<T>({
  id?: string,                        // defaults to safeRandomUUID()
  autoCommit?: boolean,               // default true -- commit after mutate()
  mutationFn: MutationFn<T>,          // (params: { transaction }) => Promise<any>
  metadata?: Record<string, unknown>, // custom data attached to the transaction
})
```

## Transaction Object

```ts
interface Transaction<T> {
  id: string
  state: 'pending' | 'persisting' | 'completed' | 'failed'
  mutations: Array<PendingMutation<T>>
  autoCommit: boolean
  createdAt: Date
  sequenceNumber: number
  metadata: Record<string, unknown>
  error?: { message: string; error: Error }

  // Deprecated alias for the settlement promise; retained until the 1.0 RC
  isPersisted: {
    promise: Promise<Transaction<T>>
    resolve: (value: Transaction<T>) => void
    reject: (reason?: any) => void
  }

  // Resolves when the transaction settles; rejects on failure or rollback
  when(state: 'settled'): Promise<Transaction<T>>

  // Execute collection operations inside the ambient transaction context
  mutate(callback: () => void): Transaction<T>

  // Commit -- calls mutationFn, transitions to persisting -> completed|failed
  commit(): Promise<Transaction<T>>

  // Rollback -- transitions to failed, also rolls back conflicting transactions
  rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T>
}
```

**Lifecycle:** `pending` -> `persisting` -> `completed` | `failed`

- `mutate()` only allowed in `pending` state (throws `TransactionNotPendingMutateError`)
- `commit()` only allowed in `pending` state (throws `TransactionNotPendingCommitError`)
- `rollback()` allowed in `pending` or `persisting` (throws `TransactionAlreadyCompletedRollbackError` if completed)
- Failed `mutationFn` automatically triggers `rollback()`
- Rollback cascades to other pending transactions sharing the same item keys
- An empty or fully cancelled transaction completes without calling `mutationFn`

## PendingMutation Type

```ts
interface PendingMutation<T, TOperation = 'insert' | 'update' | 'delete'> {
  mutationId: string // unique id for this mutation
  original: TOperation extends 'insert' ? {} : T // state before mutation
  modified: T // state after mutation
  changes: Partial<T> // only the changed fields
  key: any // collection-local key
  globalKey: string // globally unique key (collectionId + key)
  type: TOperation // "insert" | "update" | "delete"
  metadata: unknown // user-provided metadata
  syncMetadata: Record<string, unknown> // adapter-specific metadata
  optimistic: boolean // whether applied optimistically (default true)
  createdAt: Date
  updatedAt: Date
  collection: Collection // reference to the source collection
}
```

## Mutation Merging Rules

When multiple mutations target the same item (same `globalKey`) within a
transaction, they merge:

| Existing | Incoming | Result    | Notes                              |
| -------- | -------- | --------- | ---------------------------------- |
| insert   | update   | insert    | Merge changes, keep empty original |
| insert   | delete   | _removed_ | Both mutations cancel out          |
| update   | update   | update    | Union changes, keep first original |
| update   | delete   | delete    | Delete dominates                   |
| delete   | delete   | delete    | Replace with latest                |
| insert   | insert   | insert    | Replace with latest                |

`(delete, update)` and `(delete, insert)` cannot occur -- the collection
prevents operations on deleted items within the same transaction.

## getActiveTransaction / Ambient Transaction Context

```ts
import { getActiveTransaction } from '@tanstack/db'

const tx = getActiveTransaction() // Transaction | undefined
```

Inside `tx.mutate(() => { ... })`, the transaction is pushed onto an internal
stack. Any `collection.insert/update/delete` call automatically joins the
topmost ambient transaction. This is how `createOptimisticAction` and
`createPacedMutations` wire collection operations into their transactions.

The ambient scope lasts only for the synchronous `mutate()` callback. A
collection operation after an `await` does not join that transaction. Put async
work in `mutationFn`, or call `mutate()` again while the transaction is still
pending.

## createOptimisticAction

```ts
import { createOptimisticAction } from "@tanstack/db"

const action = createOptimisticAction<TVariables>({
  // Synchronous -- apply optimistic state immediately (MUST NOT return a Promise)
  onMutate: (variables: TVariables) => void,

  // Async -- persist to backend, wait for sync back
  mutationFn: (variables: TVariables, params: { transaction }) => Promise<any>,

  // Optional: same as createTransaction config
  id?: string,
  autoCommit?: boolean,    // default true; false requires manual commit()
  metadata?: Record<string, unknown>,
})

// Returns a function: (variables: TVariables) => Transaction
const tx = action(variables)
await tx.when('settled')
```

## createPacedMutations

```ts
import { createPacedMutations } from "@tanstack/db"

const mutate = createPacedMutations<TVariables>({
  onMutate: (variables: TVariables) => void,   // synchronous optimistic update
  mutationFn: MutationFn,                       // persists merged transaction
  strategy: Strategy,                            // timing control
  metadata?: Record<string, unknown>,
})

// Returns a function: (variables: TVariables) => Transaction
const tx = mutate(variables)
```

Rapid calls merge into the active transaction (via `applyMutations`) until the
strategy fires the commit. A new transaction is created for subsequent calls.

## Strategy Types

### debounceStrategy

```ts
import { debounceStrategy } from "@tanstack/db"

debounceStrategy({
  wait: number,           // ms to wait after last call before committing
  leading?: boolean,      // execute on the leading edge (default false)
  trailing?: boolean,     // execute on the trailing edge (default true)
})
```

Debounce cleanup lets a pending write run after the last call's quiet period.
It returns before that transaction settles.
With `trailing: false`, a skipped call rejects with `DebounceCallDroppedError`.

### throttleStrategy

```ts
import { throttleStrategy } from "@tanstack/db"

throttleStrategy({
  wait: number,           // minimum ms between commits
  leading?: boolean,      // defaults true unless trailing is explicitly true
  trailing?: boolean,     // defaults true; false rejects skipped optimistic calls
})
```

Throttle cleanup lets an already scheduled trailing write run at its configured
edge. It returns before that transaction settles.

### queueStrategy

```ts
import { queueStrategy } from "@tanstack/db"

queueStrategy({
  wait?: number,                      // ms between processing items (default 0)
  maxSize?: number,                   // reject overflow when waiting queue is full
  addItemsTo?: "front" | "back",     // default "back" (FIFO)
  getItemsFrom?: "front" | "back",   // default "front" (FIFO)
})
```

Queue creates a **separate transaction per call** (unlike debounce/throttle
which merge). Each transaction commits and awaits settlement before the next
starts. Failed transactions do not block subsequent ones. Cleanup drains admitted
work at the configured pace but rejects later calls with `QueueDisposedError`.

## Transaction.when('settled')

```ts
const tx = collection.insert({ id: '1', text: 'Hello' })

try {
  await tx.when('settled') // resolves with the Transaction on success
  console.log(tx.state) // "completed"
} catch (error) {
  console.log(tx.state) // "failed"
  // optimistic state has been rolled back
}
```

`when('settled')` returns the existing settlement promise. It is created at
transaction construction time and settled when `commit()` completes or
`rollback()` is called. For
`autoCommit: true` transactions, commit starts after `mutate()` returns; the
promise can remain pending as long as `mutationFn` does.

For a non-empty commit, `mutationFn` is the normal success boundary.
`when('settled')` does not by itself prove that a backend uploaded,
confirmed, or read back the write. It proves those stronger guarantees only
when `mutationFn` waits for them before returning.

The old `isPersisted.promise` remains available until the 1.0 RC but is
deprecated. Replace it with `when('settled')`.
