Debouncing
Coalesce rapid mutations for better performance.
The DebouncerMap coalesces rapid mutations on the same record into a single mutation. This is useful for scenarios like live text editing, slider adjustments, or any UI interaction that generates many updates in quick succession.
How It Works
When a debounced mutation is submitted:
- If no pending mutation exists for the key, a new timer is started.
- If a pending mutation already exists, the new record fields are merged into the pending mutation and the timer is reset.
- When the timer fires, the coalesced mutation is executed.
t=0ms set("t1", {title: "H"}) -> timer starts (300ms)
t=50ms set("t1", {title: "He"}) -> merge + reset timer
t=100ms set("t1", {title: "Hel"}) -> merge + reset timer
t=400ms -> timer fires, execute {title: "Hel"}Usage on the Client
Add debounceKey and debounceMs to a mutation:
await client.table("todos").mutate({
operation: "merge",
id: "t1",
record: { title: inputValue },
debounceKey: "t1", // Group mutations by this key
debounceMs: 300, // Wait 300ms after last keystroke
});DebouncerMap API
import { DebouncerMap } from "@datafn/client";
const debouncer = new DebouncerMap();set(key, mutation, delayMs, executor)
Schedule a debounced mutation. If a mutation with the same key is already pending, merge the records and reset the timer.
await debouncer.set(
"t1", // Debounce key
{ resource: "todos", operation: "merge", id: "t1", record: { title: "Hello" } },
300, // Delay in ms
async (mutation) => { // Executor function
await remoteMutate(mutation);
},
);Record merging: When successive mutations target the same key, their record fields are shallow-merged:
// First call
set("t1", { record: { title: "A", color: "red" } }, 300, exec);
// Second call (before timer fires)
set("t1", { record: { title: "B" } }, 300, exec);
// Executed mutation: { record: { title: "B", color: "red" } }Promise resolution: When a pending mutation is replaced, the previous caller's promise resolves immediately (data is not lost because it was merged into the new mutation).
flush(key?)
Execute the pending mutation for a specific key immediately, bypassing the timer:
await debouncer.flush("t1");flushAll()
Execute all pending mutations immediately:
await debouncer.flushAll();All mutations are executed in parallel using Promise.allSettled.
destroy()
Clear all timers without executing pending mutations. Use this when tearing down the client:
debouncer.destroy();Automatic Flush on Page Unload
The DataFn client automatically flushes all pending debounced mutations before the page unloads, ensuring no data is lost when the user navigates away.
Example: Live Text Editor
function handleTextChange(todoId: string, newTitle: string) {
client.table("todos").mutate({
operation: "merge",
id: todoId,
record: { title: newTitle },
debounceKey: todoId,
debounceMs: 500,
});
}
// Each keystroke calls handleTextChange.
// Only the final value is sent after 500ms of inactivity.Example: Slider Control
function handlePriorityChange(todoId: string, priority: number) {
client.table("todos").mutate({
operation: "merge",
id: todoId,
record: { priority },
debounceKey: `${todoId}:priority`,
debounceMs: 200,
});
}Behavior Details
- Only
mergeoperations benefit from debouncing. Other operations (insert, delete, replace) should execute immediately. - The
mutationIdfrom the first call in a debounce group is preserved for tracking. - Each debounce key is independent. Mutations on different keys do not interact.
- The executor function is called with the fully merged mutation after the delay.