Idempotency
Prevent duplicate mutations with idempotency tracking.
DataFn deduplicates mutations using a (clientId, mutationId) pair. When the same mutation is received more than once (due to retries, network issues, or duplicate pushes), the server returns the cached result instead of re-executing the mutation.
How It Works
- Every mutation includes a
clientIdandmutationId. - Before executing, the server checks the idempotency store for an existing result.
- If found, the cached result is returned with
deduped: true. - If not found, the mutation is executed and the result is stored.
// Client-generated mutation
{
resource: "todos",
operation: "insert",
id: "t1",
record: { title: "Buy milk" },
clientId: "client-abc-123",
mutationId: "mut-001",
}Idempotency Stores
DataFn provides two idempotency store implementations:
MemoryIdempotencyStore
In-memory store suitable for single-instance deployments and development:
import { MemoryIdempotencyStore } from "@datafn/server";
const store = new MemoryIdempotencyStore({
ttlMs: 86_400_000, // Entry TTL: 24 hours (default)
maxEntries: 100_000, // Max entries before LRU eviction (default)
});Characteristics:
- TTL sweep: A timer runs every 5 minutes, evicting entries older than
ttlMs. - LRU eviction: When the store reaches
maxEntries, the oldest entry (first in insertion order) is evicted on the nextset(). - LRU refresh:
get()moves the accessed entry to the end of the Map (most recently used). - No overwrite: Once a result is stored for a
(clientId, mutationId)pair, subsequentset()calls for the same pair are silently ignored. - Cleanup: Call
store.destroy()on shutdown to clear the sweep timer.
DbIdempotencyStore
Database-backed store for durable deduplication across restarts and multi-instance deployments:
import { DbIdempotencyStore } from "@datafn/server";
const store = new DbIdempotencyStore(adapter, "my-namespace");Characteristics:
- Stores results in the
__datafn_idempotencyinternal table. - Keyed by
namespace:clientId:mutationId. - Results are JSON-serialized.
- Survives server restarts.
- Supports pruning by age.
Pruning
The database store supports pruning old entries:
// Delete entries older than 7 days
const deletedCount = await store.pruneIdempotency(7);Mutation Result Type
interface MutationResult {
ok: boolean;
mutationId: string;
affectedIds: string[];
errors: Array<{
code: string;
message: string;
path: string;
retryable: boolean;
}>;
deduped: boolean; // true if this was a cached result
}Client-Side Deduplication
The client-side changelog also deduplicates by (clientId, mutationId):
- IndexedDB: Uses a unique compound index on
[clientId, mutationId]. - Memory: Uses an O(1) Map index keyed by
clientId:mutationId.
If changelogAppend is called with a duplicate pair, the existing entry is returned without creating a new one.
Best Practices
- Always include a unique
mutationIdwith every mutation. - Use UUIDs or content-based hashes for mutation IDs.
- Configure TTL based on your retry window (24 hours covers most scenarios).
- For multi-instance deployments, use
DbIdempotencyStorefor durability. - Schedule periodic pruning to prevent unbounded table growth.