Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(esl-utils): debounced unhandled rejection #1839

Merged
merged 8 commits into from
Aug 8, 2023
72 changes: 47 additions & 25 deletions src/modules/esl-utils/async/debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,43 @@ export interface Debounced<F extends AnyToAnyFnSignature> {
cancel(): void;
}

class Debouncer<F extends AnyToAnyFnSignature> {
public timeout: number | null = null;

private deferred: Deferred<ReturnType<F>> | null = null;

private promiseRequested = false;

constructor(private readonly fn: F, private readonly wait = 10, private readonly thisArg?: object) {}

public debouncedSubject(that: any, ...args: any[]): void {
(typeof this.timeout === 'number') && clearTimeout(this.timeout);
this.timeout = window.setTimeout(() => {
const fn = this.fn.apply(this.thisArg || that, args);
this.promiseRequested && this.deferred?.resolve(fn);
this.resetPromise();
}, this.wait);
}

public cancel(): void {
(typeof this.timeout === 'number') && clearTimeout(this.timeout);
this.promiseRequested && this.deferred?.reject();
this.resetPromise();
}

private resetPromise(): void {
this.timeout = null;
this.deferred = null;
this.promiseRequested = false;
}

public get promise(): Promise<ReturnType<F> | void> {
this.promiseRequested = true;
this.deferred = createDeferred();
return this.deferred.promise;
}
}

/**
* Creates a debounced function that implements {@link Debounced}.
* Debounced function delays invoking func until after wait milliseconds have elapsed
Expand All @@ -22,36 +59,21 @@ export interface Debounced<F extends AnyToAnyFnSignature> {
* @param wait - time to debounce
* @param thisArg - optional context to call original function, use debounced method call context if not defined
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export function debounce<F extends AnyToAnyFnSignature>(fn: F, wait = 10, thisArg?: object): Debounced<F> {
let timeout: number | null = null;
let deferred: Deferred<ReturnType<F>> | null = null;

function debouncedSubject(...args: any[]): void {
deferred = deferred || createDeferred();
(typeof timeout === 'number') && clearTimeout(timeout);
timeout = window.setTimeout(() => {
timeout = null;
// fn.apply to save call context
deferred!.resolve(fn.apply(thisArg || this, args));
deferred = null;
}, wait);
}
function cancel(): void {
(typeof timeout === 'number') && clearTimeout(timeout);
timeout = null;
deferred?.reject();
deferred = null;
}
const instance = new Debouncer(fn, wait, thisArg);

const debouncer = function (...args: Parameters<F>): void {
instance.debouncedSubject(this, ...args);
};

Object.defineProperty(debouncedSubject, 'promise', {
get: () => deferred ? deferred.promise : Promise.resolve()
Object.defineProperty(debouncer, 'promise', {
get: () => instance.promise
});
Object.defineProperty(debouncedSubject, 'cancel', {
Object.defineProperty(debouncer, 'cancel', {
writable: false,
enumerable: false,
value: cancel
value: () => instance.cancel()
});

return debouncedSubject as Debounced<F>;
return debouncer as Debounced<F>;
}