a throttle function, similar to throttle, that also insures that the final call (aka trailing call) made to the throttled function always resolves eventually.

this is useful in cases where it is of utmost importance that the throttled function is called one last time with before a prolonged delay.

the following visual illustration shows the difference between the regular throttle, and throttleAndTrail functions:

here is a function fn throttled with trailing_time_ms = 1500, and delta_time_ms = 1000. as you can see below, the trailing calls to the throttled function do get resolved eventually (1500ms after the last call).

│time     │         ╭╶╶╮ 1.2            2.7   3.3            4.8 5.2              
├─────────│       ╭╶┤  │ ┌───(delayed)──┐     ┌───(delayed)──┐   (rejected)       
│         │    ╭╶╶┤ │  │ │              ▼   ╭╶┤              ▼   ╭╶╶╶╶╶╶╶╮        
│resolved │  o ▼  ▼ ▼  o │              o o ▼ │              o o ▼       o        
│rejected │  │ x  x x  │ │                │ x │                │ x       │        
│call made├──┴─┴──┴─┴──┴─┴────────────────┴─┴─┴────────────────┴─┴───────┴──► time
│time     │  0         1         2         3         4         5         6        

here is a function fn throttled with delta_time_ms = 1000. as it can be seen below, the final call to the throttled function gets rejected, because it was called too quickly.

│resolved │  o         o                  o                           
│rejected │  │ x  x x  │ x                │ x x                       
│call made├──┴─┴──┴─┴──┴─┴────────────────┴─┴─┴─────────────────► time
│time     │  0         1         2         3         4         5      
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 throttling to
const fn = (v: number) => {
log_history.push([current_time(), v])
return v + 100
}

// the throttled version of `fn` with trailing enabled.
// a call is considered to be a trailing call if more than `trailing_time_ms` of 1500ms has passed since its inception.
// however after a successful call, subsequent non-trailing calls made under the `delta_time` interval of 1000ms will be rejected with the value `"REJECTED!"`.
const throttled_fn = throttleAndTrail(1500, 1000, fn, "REJECTED!")

// first call to the `throttled_fn` will be evaluated successfully and immediately (albeit being wrapped under a promise)
const a = throttled_fn(24)

await sleep(100)
assertGe(log_history[0][0], 0)
assertLe(log_history[0][0], 100)
assertEq(log_history[0][1], 24)
assertEq(await a, 100 + 24)

// subsequent non-trailing calls to the `throttled_fn` that are made under 1000ms, will be rejected with the custom value `"REJECTED!"`.
await sleep(100)
const b1 = throttled_fn(37).catch(reason => reason)
const b2 = throttled_fn(42).catch(reason => reason)

assertEq(await b1, "REJECTED!")
// we don't await for `b2`, because after 1500ms, it will be resolved due to being a trailing call, and we don't want that right now.
assertGe(current_time(), 199)
assertLe(current_time(), 400)

// finally, we create a trailing call, which will take 1500ms to resolve, since less than 1000ms has passed since the last call (`b2`).
await sleep(100)
const c = throttled_fn(99)

assertEq(await c, 100 + 99)
assertGe(log_history[1][0], 300 + 1499)
assertLe(log_history[1][0], 300 + 1700)
assertEq(log_history[1][1], 99)
assertEq(await b2, "REJECTED!")
  • Type Parameters

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

    Parameters

    • trailing_time_ms: number

      the time in milliseconds after which a trailing (pending) call to the function gets resolved if no other calls are made during that time interval. you would definitely want this to be some value greater than delta_time_ms, otherwise it will be weird because if this value is smaller, then trailing_time_ms will become the "effective" throttling time interval, but also one that always resolved later rather than immediately.

    • delta_time_ms: number

      the time interval in milliseconds for throttling

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

      the function to be throttled

    • 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 throttled function is made within the trailing_time_ms waiting period. if no rejection value is provided, then the promise will linger around unsettled forever. do note that having a rejection value means that you will have to catch any rejections if you wait for the promise, otherwise it will bubble to the top level as an unhandled error.

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

    a function (that takes arguments intended for fn) that returns a Promise to the value of fn if it is resolved (i.e. not throttled or when trailing), otherwise if throttled, then that promise will either be never be resolved, or rejected based on if a rejection_value was provided.