Skip to content

Instantly share code, notes, and snippets.

@JamieMason
Last active December 8, 2021 20:34
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JamieMason/4719468ef5629328eb2332b31f08b081 to your computer and use it in GitHub Desktop.
Save JamieMason/4719468ef5629328eb2332b31f08b081 to your computer and use it in GitHub Desktop.
Lossless Debounce Function in JavaScript

Lossless Debounce Function

Returns a function which will capture and collect all invocation arguments, to process in batches once ms consecutive resting time has passed.

Know a more common name for this function? Let me know in this discussion.

Demo

const fn = losslessDebounce(console.log, 100)
fn(1)
await sleep(50)
fn(2, 3)
await sleep(100)
// 1, 2, 3
fn(4)
fn(5)
await sleep(100)
// 4, 5

Example Use Case

Reduce the number of calls made to an upstream API, by making one call with a payload that contains multiple values, instead of multiple calls with one value each.

const updateProductsById = losslessDebounce((...productIds: string[]): void => {
  fetch('https://some-scraper.com/v2/trigger-job-in-the-background', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      ACTOR_NAME: 'UPDATE_PRODUCTS_BY_ID',
      productIds,
    }),
  });
}, 5000);
import { losslessDebounce } from './lossless-debounce';
test('losslessDebounce', () => {
jest.useFakeTimers();
const spy = jest.fn();
const fn = losslessDebounce(spy, 100);
fn(1);
jest.advanceTimersByTime(50);
expect(spy).toHaveBeenCalledTimes(0);
fn(2, 3);
jest.advanceTimersByTime(50);
expect(spy).toHaveBeenCalledTimes(0);
fn(4);
jest.advanceTimersByTime(100);
expect(spy).toHaveBeenCalledWith(1, 2, 3, 4);
expect(spy).toHaveBeenCalledTimes(1);
fn(5);
jest.advanceTimersByTime(50);
expect(spy).toHaveBeenCalledTimes(1);
fn(6, 7);
jest.advanceTimersByTime(50);
expect(spy).toHaveBeenCalledTimes(1);
fn(8);
jest.advanceTimersByTime(100);
expect(spy).toHaveBeenCalledWith(5, 6, 7, 8);
expect(spy).toHaveBeenCalledTimes(2);
fn();
jest.advanceTimersByTime(100);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(3);
});
type Fn<T> = (...args: T[]) => void;
/**
* Returns a function which will capture and collect all invocation arguments,
* to process in batches once `ms` consecutive resting time has passed.
*
* @example
* const fn = losslessDebounce(console.log, 100)
* fn(1)
* await sleep(50)
* fn(2, 3)
* await sleep(100)
* // 1, 2, 3
* fn(4)
* fn(5)
* await sleep(100)
* // 4, 5
*/
export function losslessDebounce<T>(fn: Fn<T>, ms: number): Fn<T> {
let argumentsBuffer: T[] = [];
let nextFlush: NodeJS.Timer = null;
return function collectArguments(...args) {
argumentsBuffer.push(...args);
clearTimeout(nextFlush);
nextFlush = setTimeout(flush, ms);
};
function flush() {
const args = argumentsBuffer;
argumentsBuffer = [];
fn(...args);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment