Created
September 2, 2024 11:22
-
-
Save AoiYamada/e0d3a59c2a69626cf8b188ffd9dc0de0 to your computer and use it in GitHub Desktop.
Key based debounce
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import keyBasedDebounce from '../keyBasedDebounce' | |
jest.useFakeTimers().setSystemTime(new Date('2024-09-01')) | |
describe('keyBasedDebounce', () => { | |
beforeEach(() => { | |
jest.clearAllMocks() | |
}) | |
it('should debounce the function call', () => { | |
const mockFn = jest.fn() | |
const debouncedFn = keyBasedDebounce(mockFn, (...args) => args[0], 1200) | |
debouncedFn('key1') | |
debouncedFn('key2') | |
debouncedFn('key3') | |
// Only the last call should be executed after the debounce delay | |
expect(mockFn).not.toHaveBeenCalled() | |
jest.advanceTimersByTime(1000) | |
debouncedFn('key2') // delayed | |
debouncedFn('key3') // delayed | |
expect(mockFn).not.toHaveBeenCalled() | |
jest.advanceTimersByTime(1000) | |
debouncedFn('key3') // delayed | |
expect(mockFn).toHaveBeenCalledTimes(1) | |
expect(mockFn).toHaveBeenCalledWith('key1') | |
jest.advanceTimersByTime(1000) | |
expect(mockFn).toHaveBeenCalledTimes(2) | |
expect(mockFn).toHaveBeenCalledWith('key2') | |
jest.advanceTimersByTime(1000) | |
expect(mockFn).toHaveBeenCalledTimes(3) | |
expect(mockFn).toHaveBeenCalledWith('key3') | |
}) | |
it('should execute the function immediately if leading option is true', () => { | |
const mockFn = jest.fn() | |
const debouncedFn = keyBasedDebounce(mockFn, (...args) => args[0], 2000, { | |
leading: true, | |
}) | |
debouncedFn('key1', 'value1') | |
debouncedFn('key2', 'value1') | |
debouncedFn('key3', 'value1') | |
// The first call should be executed immediately | |
expect(mockFn).toHaveBeenCalledTimes(3) | |
expect(mockFn).toHaveBeenCalledWith('key1', 'value1') | |
expect(mockFn).toHaveBeenCalledWith('key2', 'value1') | |
expect(mockFn).toHaveBeenCalledWith('key3', 'value1') | |
jest.advanceTimersByTime(1000) | |
// in leading mode, the function should not be called again when the delay is not reached | |
debouncedFn('key1', 'value2') | |
debouncedFn('key2', 'value2') | |
debouncedFn('key3', 'value2') | |
expect(mockFn).toHaveBeenCalledTimes(3) | |
jest.advanceTimersByTime(1000) | |
expect(mockFn).toHaveBeenCalledTimes(3) | |
// The function should be called again after the delay | |
debouncedFn('key1', 'value3') | |
debouncedFn('key2', 'value3') | |
debouncedFn('key3', 'value3') | |
expect(mockFn).toHaveBeenCalledTimes(6) | |
expect(mockFn).toHaveBeenCalledWith('key1', 'value3') | |
expect(mockFn).toHaveBeenCalledWith('key2', 'value3') | |
expect(mockFn).toHaveBeenCalledWith('key3', 'value3') | |
}) | |
it('should execute the function with the latest arguments', () => { | |
const mockFn = jest.fn() | |
const debouncedFn = keyBasedDebounce(mockFn, (...args) => args[0], 2000) | |
debouncedFn('key1', 'value1') | |
jest.advanceTimersByTime(1000) | |
debouncedFn('key1', 'value2') | |
jest.advanceTimersByTime(1000) | |
debouncedFn('key1', 'value3') | |
// Only the last call should be executed after the debounce delay | |
expect(mockFn).not.toHaveBeenCalled() | |
jest.runAllTimers() | |
expect(mockFn).toHaveBeenCalledTimes(1) | |
expect(mockFn).toHaveBeenCalledWith('key1', 'value3') | |
}) | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
type Fn = (...args: any[]) => any | |
type DebounceOpts = { | |
leading?: boolean | |
} | |
type Context<T extends Fn> = { | |
lastCallTime: number | |
lastArgs: Parameters<T> | |
result: ReturnType<T> | undefined | |
timeoutId: number | NodeJS.Timeout | undefined | |
} | |
function keyBasedDebounce<T extends Fn>( | |
fn: T, | |
keyFn: (...args: Parameters<T>) => string, | |
delay: number, | |
options?: DebounceOpts | |
) { | |
const { leading = false } = options || {} | |
const contexts = new Map<string, Context<T>>() | |
function debounced(...args: Parameters<T>) { | |
const key = keyFn(...args) | |
const context = ensureContext(contexts, key, args) | |
const time = Date.now() | |
const isCalling = shouldCall(time, delay, context) | |
context.lastArgs = args | |
if (leading) { | |
if (isCalling) { | |
executeFn<T>(context, time, fn) | |
} | |
} else { | |
if (context.timeoutId) { | |
clearTimeout(context.timeoutId) | |
} | |
// | |
context.timeoutId = setTimeout(() => { | |
const time = Date.now() | |
executeFn<T>(context, time, fn) | |
}, delay) | |
} | |
return context.result | |
} | |
return debounced | |
} | |
export default keyBasedDebounce | |
function ensureContext<T extends Fn>( | |
contexts: Map<string, Context<T>>, | |
key: string, | |
args: Parameters<T> | |
) { | |
let context = contexts.get(key) | |
if (!context) { | |
context = { | |
lastCallTime: 0, | |
lastArgs: args, | |
result: undefined, | |
timeoutId: undefined, | |
} | |
contexts.set(key, context) | |
} | |
return context | |
} | |
function shouldCall<T extends Fn>( | |
time: number, | |
delay: number, | |
context: Context<T> | |
) { | |
return time - context.lastCallTime >= delay | |
} | |
function executeFn<T extends Fn>(context: Context<T>, time: number, fn: T) { | |
context.lastCallTime = time | |
context.result = fn(...context.lastArgs) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment