the time interval in milliseconds for debouncing
the function to be debounced
Optional
rejection_value: REJif a rejection value is provided, then old unresolved pending promises will be rejected with the given value, when a new call to the debounced function is made (within the debouncing waiting period)
a function (that takes arguments intended for fn
) that returns a promise, which is resolved once wait_time_ms
amount of time has passed with no further calls
import {
assertEquals as assertEq,
assertLessOrEqual as assertLe,
assertGreaterOrEqual as assertGe,
} from "jsr:@std/assert"
// a function that creates an asynchronous delay of the given number of milliseconds
const sleep = (time_ms: number) => (new Promise((resolve) => (setTimeout(resolve, time_ms))))
const
log_history: Array<[time: number, value: number]> = [],
error_history: Array<[time: number, message: string]> = [],
t0 = performance.now(),
current_time = () => (performance.now() - t0)
// the function that we plan to debounce
const fn = (v: number) => {
log_history.push([current_time(), v])
return v + 100
}
// the debounced version of `fn`, that, when called too quickly, rejects with the reason `"SEPPUKU!"`.
const debounced_fn = debounce(1000, fn, "SEPPUKU!")
// `a` is a promise that should resolve after 1000ms. but if it fails (which it will), it will log to `error_history`.
const a = debounced_fn(24).catch(
(reason) => { error_history.push([current_time(), `they want me to ${reason}`]) }
)
await sleep(500) // promise `a` is still pending after the 500ms sleep
// the new `debounced_fn(42)` below rejects `a`'s promise, and then returns a new promise (`b`) that will resolve in 1000ms.
// since `a` gets rejected as a consequence, it's error handling chain will immediately log `"they want me to SEPPUKU!"` in the console.
const b = debounced_fn(42)
// we delay for a tiny while in order for the js-runtime to get `a` rejected. otherwise, without the sleep, `error_history` will be empty.
await sleep(50)
assertEq(log_history, [])
assertGe(error_history[0][0], 499)
assertLe(error_history[0][0], 540)
assertEq(error_history[0][1], "they want me to SEPPUKU!")
// after 1000ms, the value `42` will be logged into `log_history` (due to promise `b`).
// however, no entry will be logged by `a`, since it gets rejected as soon as `b` is created.
await sleep(1000)
assertGe(log_history[0][0], 500 + 999)
assertLe(log_history[0][0], 500 + 1040)
assertEq(log_history[0][1], 42)
assertEq(await b, 100 + 42)
// we can now safely create a new debounced call without rejecting the already resolved `b` promise, since it is over 1000ms since `b` was created.
const c = debounced_fn(99)
// 1000ms later, the value `99` will be logged into `log_history` (due to promise `c`).
await sleep(1050)
assertGe(log_history[1][0], 1550 + 999)
assertLe(log_history[1][0], 1550 + 1100)
assertEq(log_history[1][1], 99)
assertEq(await c, 100 + 99)
creates a debounced version of the provided function that returns a new promise.
the debounced function delays the execution of the provided function
fn
until the debouncing intervalwait_time_ms
amount of time has passed without any subsequent calls.if a
rejection_value
is provided, then any subsequent calls to the debounced function that are made within the debouncing interval, will reject the previous promises. thus you will have tocatch
them in that case (otherwise it will result in an error).you may worry that too many calls to a non-rejectable debounced function (i.e. when
rejection_value === undefined
) will create too many promise objects, possibly resulting in memory leaks. however, luckily, modern javascript engines are not afflicted by too many pending promise objects. in fact, choosing to reject promises (i.e. by settingrejection_value
), might be more expensive down the line, as error catching is typically expensive.also check out debounceAndShare, which avoids this "lots of promise objects" issue by sharing the same promise across all quick callers of the debounce. but it will require careful usage, as all promised callers will eventually get resolved, which may create an unintended avalaunch of subsequent
then
calls if not used carefully.