diff --git a/.changeset/clarify-queue-docs.md b/.changeset/clarify-queue-docs.md new file mode 100644 index 000000000..2230dbacb --- /dev/null +++ b/.changeset/clarify-queue-docs.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Clarify queueStrategy error handling behavior in documentation. Changed "guaranteed to persist" to "guaranteed to be attempted" and added explicit documentation about how failed mutations are handled (not retried, queue continues). Added new Retry Behavior section with example code for implementing custom retry logic. diff --git a/docs/guides/mutations.md b/docs/guides/mutations.md index 4c1662560..35615e249 100644 --- a/docs/guides/mutations.md +++ b/docs/guides/mutations.md @@ -1093,7 +1093,7 @@ function VolumeSlider() { ### Queue Strategy -The queue strategy creates a separate transaction for each mutation and processes them sequentially in order. Unlike debounce/throttle, **every mutation is guaranteed to persist**, making it ideal for workflows where you can't lose any operations. +The queue strategy creates a separate transaction for each mutation and processes them sequentially in order. Unlike debounce/throttle which may drop intermediate mutations, **every mutation is guaranteed to be attempted**, making it ideal for workflows where you can't skip any operations. ```tsx import { usePacedMutations, queueStrategy } from "@tanstack/react-db" @@ -1136,9 +1136,16 @@ function FileUploader() { - Each mutation becomes its own transaction - Processes sequentially in order (FIFO by default) - Can configure to LIFO by setting `getItemsFrom: 'back'` -- All mutations guaranteed to persist +- All mutations guaranteed to be attempted (unlike debounce/throttle which may skip intermediate mutations) - Waits for each transaction to complete before starting the next +**Error handling**: +- If a mutation fails, **it is not automatically retried** - the transaction transitions to "failed" state +- Failed mutations surface their error via `transaction.isPersisted.promise` (which will reject) +- **Subsequent mutations continue processing** - a single failure does not block the queue +- Each mutation is independent; there is no all-or-nothing transaction semantics across multiple mutations +- To implement retry logic, see [Retry Behavior](#retry-behavior) + ### Choosing a Strategy Use this guide to pick the right strategy for your use case: @@ -1453,6 +1460,45 @@ If an error occurs: `pending` → `persisting` → `failed` Failed transactions automatically rollback their optimistic state. +### Retry Behavior + +**Important:** TanStack DB does not automatically retry failed mutations. If a mutation fails (network error, server error, etc.), the transaction transitions to `failed` state and the optimistic state is rolled back. This is by design—automatic retry logic varies significantly based on your use case (idempotency requirements, error types, backoff strategies, etc.). + +To implement retry logic, wrap your API calls in your `mutationFn`: + +```typescript +// Simple retry helper +async function withRetry( + fn: () => Promise, + maxRetries = 3, + delay = 1000 +): Promise { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await fn() + } catch (error) { + if (attempt === maxRetries - 1) throw error + await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1))) + } + } + throw new Error('Unreachable') +} + +// Use in your collection +const todoCollection = createCollection({ + id: "todos", + onUpdate: async ({ transaction }) => { + const mutation = transaction.mutations[0] + // Retry up to 3 times with exponential backoff + await withRetry(() => + api.todos.update(mutation.original.id, mutation.changes) + ) + }, +}) +``` + +For more sophisticated retry strategies, consider using a library like [p-retry](https://github.com/sindresorhus/p-retry) which supports exponential backoff, custom retry conditions, and abort signals. + ## Handling Temporary IDs When inserting new items into collections where the server generates the final ID, you'll need to handle the transition from temporary to real IDs carefully to avoid UI issues and operation failures. diff --git a/packages/db/src/strategies/queueStrategy.ts b/packages/db/src/strategies/queueStrategy.ts index 2f67848f4..2a374d8ee 100644 --- a/packages/db/src/strategies/queueStrategy.ts +++ b/packages/db/src/strategies/queueStrategy.ts @@ -6,9 +6,15 @@ import type { Transaction } from '../transactions' * Creates a queue strategy that processes all mutations in order with proper serialization. * * Unlike other strategies that may drop executions, queue ensures every - * mutation is processed sequentially. Each transaction commit completes before + * mutation is attempted sequentially. Each transaction commit completes before * the next one starts. Useful when data consistency is critical and - * every operation must complete in order. + * every operation must be attempted in order. + * + * **Error handling behavior:** + * - If a mutation fails, it is NOT automatically retried - the transaction transitions to "failed" state + * - Failed mutations surface their error via `transaction.isPersisted.promise` (which will reject) + * - Subsequent mutations continue processing - a single failure does not block the queue + * - Each mutation is independent; there is no all-or-nothing transaction semantics * * @param options - Configuration for queue behavior (FIFO/LIFO, timing, size limits) * @returns A queue strategy instance