UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

234 lines (180 loc) • 8.49 kB
# 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')`.