Skip to content

Instantly share code, notes, and snippets.

@erikunha
Last active September 22, 2022 12:20
Show Gist options
  • Save erikunha/9d88c2885f44a600e53ed31ecc50230e to your computer and use it in GitHub Desktop.
Save erikunha/9d88c2885f44a600e53ed31ecc50230e to your computer and use it in GitHub Desktop.
Throttle function
import { throttle } from './throttle';
beforeEach(() => {
jest.useFakeTimers('modern');
});
describe('helpers/throttle', () => {
it('when delayed', () => {
const fn = jest.fn();
const throttleFn = throttle(fn, 3000);
setInterval(throttleFn, 100);
jest.advanceTimersByTime(10000);
expect(fn).toHaveBeenCalledTimes(3);
});
it('when leading', () => {
const fn = jest.fn();
const throttleFn = throttle(fn, {
delay: 3000,
leading: true,
});
setInterval(throttleFn, 100);
jest.advanceTimersByTime(10000);
expect(fn).toHaveBeenCalledTimes(4);
});
it('when trailing', () => {
const fn = jest.fn();
const throttleFn = throttle(fn, {
delay: 3000,
leading: false,
trailing: true,
});
const timer = setInterval(throttleFn, 100);
jest.advanceTimersByTime(12000);
clearInterval(timer);
jest.advanceTimersByTime(12000);
expect(fn).toHaveBeenCalledTimes(4);
});
it('when leading and trailing', () => {
const fn = jest.fn();
const throttleFn = throttle(fn, {
delay: 3000,
leading: true,
trailing: true,
});
const timer = setInterval(throttleFn, 100);
jest.advanceTimersByTime(12000);
clearInterval(timer);
jest.advanceTimersByTime(12000);
expect(fn).toHaveBeenCalledTimes(5);
});
it('when cancel', () => {
const fn = jest.fn();
const throttleFn = throttle(fn, 3000);
setInterval(throttleFn, 100);
throttleFn.cancel();
jest.advanceTimersByTime(12000);
expect(fn).not.toBeCalled();
});
});
import { ThrottleOptions, isThrottleOptions } from './throttleOptions';
export const throttle = (
callback: () => void,
options: ThrottleOptions | number,
) => {
let delay: number;
const leading = (options as ThrottleOptions).leading || false;
const trailing = (options as ThrottleOptions).trailing || false;
let timeoutID: NodeJS.Timeout | undefined;
let cancelled: boolean;
let lastExec = leading ? 0 : Date.now();
if (isThrottleOptions(options)) {
delay = options.delay;
} else {
delay = options;
}
const clearExistingTimeout = () => {
if (timeoutID) {
clearTimeout(timeoutID);
timeoutID = undefined;
}
};
const cancel = () => {
clearExistingTimeout();
cancelled = true;
};
const trailingExec = (exec: () => void) => {
if (trailing) {
clearExistingTimeout();
if (!timeoutID) {
timeoutID = setTimeout(exec, delay);
}
}
};
const wrapper = (...args: []) => {
if (cancelled) {
return;
}
const exec = () => {
lastExec = Date.now();
callback.apply(this, args);
};
trailingExec(exec);
const runTime = Date.now() - lastExec;
if (runTime > delay) {
exec();
}
};
wrapper.cancel = cancel;
return wrapper;
};
/**
* Config options for the implementation of throttle behaviour.
*
* @param delay the waiting time in milliseconds;
* @param leading if it's true, the function will exec on the first call;
* @param trailing if it's true, the function will exec after last call;
*/
export interface ThrottleOptions {
delay: number;
leading?: boolean;
trailing?: boolean;
}
export const isThrottleOptions = (
arg: ThrottleOptions | number,
): arg is ThrottleOptions => {
return (arg as ThrottleOptions).delay !== undefined;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment