Purely functional debounce utilities for delayed operations.
This module provides a composable way to debounce operations within GenServers without spreading timer management code throughout the codebase.
Usage in GenServer State
Add a debounce field to your state and use the functions to manage it:
defmodule MyServer do
use GenServer
alias Raxol.Core.Utils.Debounce
defstruct [:data, :debounce]
def init(_) do
{:ok, %__MODULE__{data: %{}, debounce: Debounce.new()}}
end
def handle_call({:update, value}, _from, state) do
new_state = %{state | data: value}
# Schedule save after 1 second, cancels any pending save
{debounce, _ref} = Debounce.schedule(state.debounce, :save, 1000)
{:reply, :ok, %{new_state | debounce: debounce}}
end
def handle_info({:debounce, :save}, state) do
do_save(state.data)
{:noreply, %{state | debounce: Debounce.clear(state.debounce, :save)}}
end
endKey Design Decisions
- Purely functional: All functions return new state, no side effects except scheduling
- Composable: Works with any GenServer state structure
- Explicit: Timer refs are tracked, making cancellation reliable
- Idiomatic: Uses pattern matching and tuples, not exceptions
Summary
Functions
Cancels a pending debounced operation.
Cancels all pending debounced operations.
Clears a debounce entry after it has fired.
Immediately triggers a pending debounced operation.
Creates a new debounce state.
Checks if there's a pending operation for the given key.
Returns all pending debounce keys.
Schedules a debounced operation.
Checks if a debounce message is still valid (not stale).
Types
Functions
Cancels a pending debounced operation.
Returns the updated debounce state.
Examples
debounce = Debounce.cancel(debounce, :save)
Cancels all pending debounced operations.
Useful in GenServer terminate callbacks.
Examples
def terminate(_reason, state) do
Debounce.cancel_all(state.debounce)
:ok
end
Clears a debounce entry after it has fired.
Call this in your handle_info after processing the debounced message.
Examples
def handle_info({:debounce, :save, id}, state) do
if Debounce.valid?(state.debounce, :save, id) do
do_save(state.data)
{:noreply, %{state | debounce: Debounce.clear(state.debounce, :save)}}
else
# Stale timer, ignore
{:noreply, state}
end
end
Immediately triggers a pending debounced operation.
Cancels the timer and returns {:fire, key} if there was a pending operation,
or :nothing if there was no pending operation.
The caller is responsible for executing the operation.
Examples
case Debounce.fire_now(debounce, :save) do
{:fire, :save, debounce} ->
do_save(state.data)
%{state | debounce: debounce}
{:nothing, debounce} ->
state
end
@spec new() :: %Raxol.Core.Utils.Debounce{ids: %{}, timers: %{}}
Creates a new debounce state.
Examples
iex> Debounce.new()
%Debounce{timers: %{}, ids: %{}}
Checks if there's a pending operation for the given key.
Examples
if Debounce.pending?(debounce, :save) do
Logger.debug("Save is pending")
end
Returns all pending debounce keys.
Examples
keys = Debounce.pending_keys(debounce)
# => [:save, :sync]
Schedules a debounced operation.
Cancels any existing timer for the given key and schedules a new one.
The message sent will be {:debounce, key}.
Returns the updated debounce state and the timer reference.
Examples
# Schedule a save operation after 1 second
{debounce, ref} = Debounce.schedule(debounce, :save, 1000)
# Schedule with custom message (sent as {:debounce, key, data})
{debounce, ref} = Debounce.schedule(debounce, :save, 1000, data: changes)
Checks if a debounce message is still valid (not stale).
Use this to ignore messages from cancelled timers.
Examples
def handle_info({:debounce, :save, id}, state) do
if Debounce.valid?(state.debounce, :save, id) do
do_save(state.data)
end
{:noreply, state}
end