Skip to content

Instantly share code, notes, and snippets.

@iAmNathanJ
Last active March 27, 2024 17:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iAmNathanJ/a58a3f4bba422d33cd4ec48e458b5ad3 to your computer and use it in GitHub Desktop.
Save iAmNathanJ/a58a3f4bba422d33cd4ec48e458b5ad3 to your computer and use it in GitHub Desktop.
Debounced Promise

debounced-promise.ts

Provides a debounced function that returns a handle to a Promise which can be awaited by contextually separate callers and resolves only after the last non-debounced async operation is settled.

By default, if a given call gets debounced, subsequent call(s) will ignore the previous arguments that were provided. This can be overridden by providing a mergeArguments function that allows for the finally exectuted function to batch arguments from all invocations.

Usage in React

https://stackblitz.com/edit/debounce-promise?file=src%2FApp.tsx

interface Deferred<T = any> extends Promise<T> {
resolve: (value?: T) => void;
reject: (reason?: any) => void;
}
function deferred<T>(): Deferred<T> {
const settlers: any = {};
const promise = new Promise((resolve, reject) => {
settlers.resolve = resolve;
settlers.reject = reject;
});
return Object.assign(promise, settlers);
}
type AsyncFunction = (...args: any[]) => Promise<unknown>;
interface DebounceOptions<T extends AsyncFunction> {
leading?: boolean;
mergeArguments?: (
newArguments: Parameters<T>,
previousArguments: Parameters<T>
) => Parameters<T>;
}
type DeferredDebounce<T extends AsyncFunction> = {
(...args: Parameters<T>): Deferred<Awaited<ReturnType<T>> | never>;
cancel: (reason?: any) => void;
};
function debouncePromise<T extends AsyncFunction>(
fn: T,
timeout = 300,
{ leading, mergeArguments = (newArgs) => newArgs }: DebounceOptions<T> = {}
) {
let timer: number | undefined;
let promise: Deferred | null = null;
let latestInvocation = 0;
let latestArgs: Parameters<T>;
const debounced: DeferredDebounce<T> = (...args) => {
clearTimeout(timer);
promise ??= deferred();
latestInvocation++;
latestArgs = mergeArguments(args, latestArgs ?? []);
const thisInvocation = latestInvocation;
const execute = () => {
fn(...latestArgs)
.then((result) => {
if (thisInvocation === latestInvocation) {
promise?.resolve(result);
promise = null;
latestInvocation = 0;
latestArgs = [];
}
})
.catch((err: Error) => {
promise?.reject(err);
promise = null;
latestInvocation = 0;
latestArgs = [];
});
};
if (!timer && leading) {
execute();
timer = setTimeout(() => {
timer = undefined;
}, timeout);
} else {
timer = setTimeout(execute, timeout);
}
return promise;
};
debounced.cancel = (reason?: any) => {
clearTimeout(timer);
promise?.resolve(reason);
promise = null;
};
return debounced;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment