Lodash debounce cancellation behavior during active trailing edge timers
0 reputation · 14 Jan 2020, 13:02 UTC
0 reputation · 14 Jan 2020, 13:02 UTC
The _.debounce function provides a .cancel() method to abort pending executions. When configured with trailing: true, the wrapper maintains a timer to ensure the final call in a sequence is executed after the wait period.
There is a design uncertainty regarding the state of the internal timer when .cancel() is invoked. Specifically, it is unclear if the cancellation mechanism provides a way to determine if a trailing edge execution was actually pending or if the timer was already idle at the moment of the call.
In high-frequency event environments, relying solely on .cancel() without a maxWait configuration can lead to execution starvation, yet the current API does not return a boolean or status indicator upon cancellation.
.cancel() explicitly reset the internal state to prevent any subsequent trailing calls from the previous sequence?35275 reputation · 14 Jan 2020, 13:25 UTC
Yes. The .cancel() method explicitly resets the internal state. It clears any active trailing-edge timeout and resets internal bookkeeping variables (such as lastCallTime and timeoutId), ensuring that the next invocation starts a completely fresh wait period.
No. There is no documented method or property to verify if a debounced function was actively waiting to execute before cancellation. The .cancel() method returns the debounced function itself to allow for chaining; it does not return a boolean or status indicator.
In Lodash 4.x, the _.debounce wrapper manages its state via closures. The following behaviors are established:
.cancel() nullifies the internal timer and clears the stored arguments (lastArgs) and context (lastThis).maxWait is configured, .cancel() clears both the standard trailing timer and the maximum wait timer.@types/lodash) confirm that the return type of .cancel() is the debounced function, not a status flag.Since the internal state is closed over and inaccessible, you must implement a wrapper if your application requires knowledge of whether a call was cancelled. The following pattern tracks the pending state externally:
function createTrackableDebounce(fn, wait, options) {
let isPending = false;
const debounced = _.debounce((...args) => {
isPending = false;
fn(...args);
}, wait, options);
const wrapper = (...args) => {
isPending = true;
return debounced(...args);
};
wrapper.cancel = () => {
const wasPending = isPending;
debounced.cancel();
isPending = false;
return wasPending; // Returns true if a timer was active
};
wrapper.flush = () => {
isPending = false;
return debounced.flush();
};
return wrapper;
}
const d = _.debounce(fn, 100, { trailing: true }).d(), then immediately invoke d.cancel().d() again after 50ms. If the callback executes ~100ms after this second call (rather than 50ms), the internal state was successfully reset.Are you utilizing the maxWait option? While it does not change the return value of .cancel(), it introduces a secondary timer that is also silently cleared during cancellation.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 14 Jan 2020, 13:55 UTC
Calling .cancel() on a Lodash 4.x debounced function invokes clearTimeout on the internal timer and resets the saved lastArgs, lastThis, and lastCallTime references. This guarantees that no trailing-edge callback from the prior invocation sequence will fire after the call returns.
The method returns undefined; there is no flag, promise, or callback to tell you whether a timer was actually running at the moment of cancellation. If you need that signal, wrap the debounced function and track a local isPending boolean that you set in the debounce wrapper's leading/trailing logic and clear in .cancel().
For high-frequency streams, prefer maxWait over manual .cancel() polling. maxWait forces execution at a hard ceiling, eliminating the indefinite-delay scenario without extra bookkeeping.