creates a debounced version of the provided function that returns a shared promise.

unlike conventional debounce, this function reuses and returns the same promise object for all calls that are made within the debouncing interval. this means that all callers within this interval will receive the same promise, which will be resolved once wait_time_ms amount of time has passed with no further calls.

if subsequent calls are made within the debouncing interval, the debounced function will return the same promise as before, further delaying its resolution. however, once the debouncing interval has elapsed and the promise is resolved, any new calls to the debounced function will create and return a new promise.

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]> = [],
t0 = performance.now(),
current_time = () => (performance.now() - t0)

// the function that we plan to apply shareable-debouncing to
const fn = (v: number) => {
log_history.push([current_time(), v])
return v + 100
}

// the debounced version of `fn`, that, when called too quickly, shares the existing promise, and prolongs its resolution by the wait time
const debounced_fn = debounceAndShare(1000, fn)

// `a` is a promise that should resolve after 1000ms since the last interruption call to `debounced_fn` (i.e. 1000ms from now).
const a = debounced_fn(24)

await sleep(500) // promise `a` is still pending after the 500ms sleep

// since `a` has not been resolved yet, calling `debounced_fn` again to construct `b` will result in the reusage of the old existing promise `a`.
// in addition, due to the interuptive call, the resolution of the `a` and `b` promises will be delayed by another 1000ms from here on.
const b = debounced_fn(42)

assertEq(a === b, true) // the promises `a` and `b` are one and the same, since `debounced_fn` shares the existing non-settled promises.

// after the sleep below, 1050ms would have passed since the creation of `a`, and it would have been resolved had we _not_ created `b`.
// however, since we created `b` within the debounce interval of 1000ms, the promise's timer has been reset.
await sleep(550)
assertEq(log_history, [])

// 1000ms after the creation of `b`, the value `42` will be logged into `log_history` (due to promise `a` and `b`).
await sleep(500)
assertGe(log_history[0][0], 500 + 999)
assertLe(log_history[0][0], 500 + 1040)
assertEq(log_history[0][1], 42)
assertEq(await a, 100 + 42) // notice that the value of `a` has changed to the expected output value of `b`, because they are one and the same.
assertEq(await b, 100 + 42)

// now that promises `a` and `b` have been resolved, executing the shared-debounced function again will create a new promise that will resolve after 1000ms from now.
const c = debounced_fn(99)
assertEq(c === b, false)

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

// key takeaway:
// - notice that the promises made within the debounce interval are the same pomise objects (i.e. `a === b`).
// - however, once out of that interval, an entirely new promise is generated (i.e. `b !== c`)
  • Type Parameters

    • T extends unknown
    • ARGS extends any[]

    Parameters

    • wait_time_ms: number

      the time interval in milliseconds for debouncing

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

      the function to be debounced

    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