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 interval wait_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 to catch 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 setting rejection_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.

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)
  • Type Parameters

    • T extends unknown
    • ARGS extends any[]
    • REJ

    Parameters

    • wait_time_ms: number

      the time interval in milliseconds for debouncing

    • fn: (...args: ARGS) => T

      the function to be debounced

    • Optionalrejection_value: REJ

      if 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)

    Returns (...args: ARGS) => Promise<T>

    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