@autarcgmbh/offline-transactions
v0.1.0
Published
Offline-first transaction capabilities for TanStack DB
Readme
@tanstack/offline-transactions
Offline-first transaction capabilities for TanStack DB that provides durable persistence of mutations with automatic retry when connectivity is restored.
Features
- Outbox Pattern: Persist mutations before dispatch for zero data loss
- Automatic Retry: Exponential backoff with jitter for failed transactions
- Multi-tab Coordination: Leader election ensures safe storage access
- FIFO Sequential Processing: Transactions execute one at a time in creation order
- Flexible Storage: IndexedDB with localStorage fallback
- Type Safe: Full TypeScript support with TanStack DB integration
Installation
npm install @tanstack/offline-transactionsQuick Start
import { startOfflineExecutor } from "@tanstack/offline-transactions"
// Setup offline executor
const offline = startOfflineExecutor({
collections: { todos: todoCollection },
mutationFns: {
syncTodos: async ({ transaction, idempotencyKey }) => {
await api.saveBatch(transaction.mutations, { idempotencyKey })
},
},
onLeadershipChange: (isLeader) => {
if (!isLeader) {
console.warn("Running in online-only mode (another tab is the leader)")
}
},
})
// Create offline transactions
const offlineTx = offline.createOfflineTransaction({
mutationFnName: "syncTodos",
autoCommit: false,
})
offlineTx.mutate(() => {
todoCollection.insert({
id: crypto.randomUUID(),
text: "Buy milk",
completed: false,
})
})
// Execute with automatic offline support
await offlineTx.commit()Core Concepts
Outbox-First Persistence
Mutations are persisted to a durable outbox before being applied, ensuring zero data loss during offline periods:
- Mutation is persisted to IndexedDB/localStorage
- Optimistic update is applied locally
- When online, mutation is sent to server
- On success, mutation is removed from outbox
Multi-tab Coordination
Only one tab acts as the "leader" to safely manage the outbox:
- Leader tab: Full offline support with outbox persistence
- Non-leader tabs: Online-only mode for safety
- Leadership transfer: Automatic failover when leader tab closes
FIFO Sequential Processing
Transactions are processed one at a time in the order they were created:
- Sequential execution: All transactions execute in FIFO order
- Dependency safety: Avoids conflicts between transactions that may reference each other
- Predictable behavior: Transactions complete in the exact order they were created
API Reference
startOfflineExecutor(config)
Creates and starts an offline executor instance.
interface OfflineConfig {
collections: Record<string, Collection>
mutationFns: Record<string, MutationFn>
storage?: StorageAdapter
maxConcurrency?: number
jitter?: boolean
beforeRetry?: (transactions: OfflineTransaction[]) => OfflineTransaction[]
onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void
onLeadershipChange?: (isLeader: boolean) => void
}OfflineExecutor
Properties
isOfflineEnabled: boolean- Whether this tab can persist offline transactions
Methods
createOfflineTransaction(options)- Create a manual offline transactionwaitForTransactionCompletion(id)- Wait for a specific transaction to completeremoveFromOutbox(id)- Manually remove transaction from outboxpeekOutbox()- View all pending transactionsnotifyOnline()- Manually trigger retry executiondispose()- Clean up resources
Error Handling
Use NonRetriableError for permanent failures:
import { NonRetriableError } from "@tanstack/offline-transactions"
const mutationFn = async ({ transaction }) => {
try {
await api.save(transaction.mutations)
} catch (error) {
if (error.status === 422) {
throw new NonRetriableError("Invalid data - will not retry")
}
throw error // Will retry with backoff
}
}Advanced Usage
Custom Storage Adapter
import {
IndexedDBAdapter,
LocalStorageAdapter,
} from "@tanstack/offline-transactions"
const executor = startOfflineExecutor({
// Use custom storage
storage: new IndexedDBAdapter("my-app", "transactions"),
// ... other config
})Custom Retry Policy
const executor = startOfflineExecutor({
maxConcurrency: 5,
jitter: true,
beforeRetry: (transactions) => {
// Filter out old transactions
const cutoff = Date.now() - 24 * 60 * 60 * 1000 // 24 hours
return transactions.filter((tx) => tx.createdAt.getTime() > cutoff)
},
// ... other config
})Manual Transaction Control
const tx = executor.createOfflineTransaction({
mutationFnName: "syncData",
autoCommit: false,
})
tx.mutate(() => {
collection.insert({ id: "1", text: "Item 1" })
collection.insert({ id: "2", text: "Item 2" })
})
// Commit when ready
await tx.commit()Migration from TanStack DB
This package uses explicit offline transactions to provide offline capabilities:
// Before: Standard TanStack DB (online only)
todoCollection.insert({ id: "1", text: "Buy milk" })
// After: Explicit offline transactions
const offline = startOfflineExecutor({
collections: { todos: todoCollection },
mutationFns: {
syncTodos: async ({ transaction }) => {
await api.sync(transaction.mutations)
},
},
})
const tx = offline.createOfflineTransaction({ mutationFnName: "syncTodos" })
tx.mutate(() => todoCollection.insert({ id: "1", text: "Buy milk" }))
await tx.commit() // Works offline!Browser Support
- IndexedDB: Modern browsers (primary storage)
- localStorage: Fallback for limited environments
- Web Locks API: Chrome 69+, Firefox 96+ (preferred leader election)
- BroadcastChannel: All modern browsers (fallback leader election)
License
MIT
