debounce
Waits for the source to go quiet, then emits its last value once.
debounce(source, ms);
debounce(source, { ms, leading: true });The operator preserves the kind of the source: a store in — a store out (holding the last settled value, starting from the source's declaration initial); an event in — an event out.
Search as you type
The main case. Take the raw input store, get a store that settles only when the typing pauses:
import { computed, store } from "@virentia/core";
import { debounce } from "@virentia/core/utils";
const query = store("");
const settled = debounce(query, 300); // Store<string>
const results = computed(() => search(list.value, settled.value));results recomputes once per pause, not once per keystroke — and everything subscribed to it re-renders once per pause too. settled is an ordinary store: derive from it, subscribe to it, read it in a scope.
Autosave a draft
The event form. Save when the user stops editing:
const draftChanged = event<Draft>();
const settled = debounce(draftChanged, 2000); // Event<Draft>
reaction({ on: settled, run: (draft) => void saveDraftFx(draft) });Every edit within two seconds pushes the save further away; one saveDraftFx call per editing burst.
Act now, ignore the burst
leading: true inverts the edge: the first hit passes immediately, then silence until a full quiet window:
const refresh = debounce(refreshClicked, { ms: 1000, leading: true });The first click refreshes right away; frantic re-clicking does nothing until the user calms down for a second.
When to reach for it
- Text inputs that drive anything expensive — search, validation against a server, filtering a large list.
- Autosave, "seen" markers, презенс-updates — anything that should fire once per burst of activity, with the final value.
- With
leading— buttons that should respond instantly but not repeatedly.
When not: if you need a steady rate during the activity (progress while scrolling), that is throttle; if you just need "later", that is delay.
Behaviour notes
- Windows are per scope. Typing in one scope neither delays nor flushes another — a test and an SSR request each get their own quiet window.
- Awaiting the source does not include the debounced emission. It is a new root update
mslater; a failure downstream of it is reported through the error funnel, never an unhandled rejection. - Owner dispose cancels the pending window — nothing fires after teardown.
- Fake timers just work: the operator sits on plain
setTimeout, sovi.useFakeTimers()+vi.advanceTimersByTime(300)drive it deterministically.