diff --git a/src/lib/utils/__tests__/defer.ts b/src/lib/utils/__tests__/defer.ts new file mode 100644 index 0000000000..159e06a119 --- /dev/null +++ b/src/lib/utils/__tests__/defer.ts @@ -0,0 +1,54 @@ +import defer from '../defer'; + +describe('defer', () => { + it('defers the call to the function', async () => { + const fn = jest.fn(); + const deferred = defer(fn); + + deferred(); + + expect(fn).toHaveBeenCalledTimes(0); + + await Promise.resolve(); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('deduplicates the calls to the function', async () => { + const fn = jest.fn(); + const deferred = defer(fn); + + deferred(); + deferred(); + deferred(); + + expect(fn).toHaveBeenCalledTimes(0); + + await Promise.resolve(); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('deduplicates the calls only until the next microtask', async () => { + const fn = jest.fn(); + const deferred = defer(fn); + + deferred(); + deferred(); + deferred(); + + expect(fn).toHaveBeenCalledTimes(0); + + await Promise.resolve(); + + expect(fn).toHaveBeenCalledTimes(1); + + deferred(); + deferred(); + deferred(); + + await Promise.resolve(); + + expect(fn).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/lib/utils/defer.ts b/src/lib/utils/defer.ts new file mode 100644 index 0000000000..0a19399a3a --- /dev/null +++ b/src/lib/utils/defer.ts @@ -0,0 +1,19 @@ +const nextMicroTask = Promise.resolve(); + +type Callback = (...args: any[]) => void; + +const defer = (callback: Callback): Callback => { + let progress: Promise | null = null; + return (...args) => { + if (progress !== null) { + return; + } + + progress = nextMicroTask.then(() => { + callback(...args); + progress = null; + }); + }; +}; + +export default defer; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index c8a92dd3c5..749db3c711 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,4 +1,5 @@ export { default as capitalize } from './capitalize'; +export { default as defer } from './defer'; export { default as isDomElement } from './isDomElement'; export { default as getContainerNode } from './getContainerNode'; export { default as isSpecialClick } from './isSpecialClick';