Skip to content

Commit bcd7268

Browse files
committed
refactor(memoization): remove misnamed memoizeDebounced
The function was fundamentally broken: it returned synchronously via `return memoized(...args)` on every call, which is just `memoize` with extra allocated `setTimeout` timers. The scheduled callback's return value was discarded, so no debouncing occurred — fn fired on every call, the cache-hit behavior was a consequence of `memoize`, not the debounce machinery. The `wait` parameter was effectively dead. A correct debounce cannot return synchronously — the whole point is to defer work past a quiet window. Changing the signature to `Promise<Result>` would be a breaking change with no existing callers (zero in-repo use sites, zero in the fleet), so the simplest fix is to remove the export. Callers that actually need debouncing should use a real debouncer that returns a promise.
1 parent ef82a5e commit bcd7268

2 files changed

Lines changed: 0 additions & 102 deletions

File tree

src/memoization.ts

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -374,46 +374,6 @@ export function memoizeAsync<Args extends unknown[], Result>(
374374
}
375375
}
376376

377-
/**
378-
* Create a debounced memoized function.
379-
* Combines memoization with debouncing for expensive operations.
380-
*
381-
* @param fn - Function to memoize and debounce
382-
* @param wait - Debounce wait time in milliseconds
383-
* @param options - Memoization options
384-
* @returns Debounced memoized function
385-
*
386-
* @example
387-
* import { memoizeDebounced } from '@socketsecurity/lib/memoization'
388-
*
389-
* const search = memoizeDebounced(
390-
* (query: string) => performSearch(query),
391-
* 300,
392-
* { name: 'search' }
393-
* )
394-
*/
395-
export function memoizeDebounced<Args extends unknown[], Result>(
396-
fn: (...args: Args) => Result,
397-
wait: number,
398-
options: MemoizeOptions<Args, Result> = {},
399-
): (...args: Args) => Result {
400-
const memoized = memoize(fn, options)
401-
let timeoutId: NodeJS.Timeout | undefined
402-
403-
return function debounced(...args: Args): Result {
404-
if (timeoutId) {
405-
clearTimeout(timeoutId)
406-
}
407-
408-
timeoutId = setTimeout(() => {
409-
memoized(...args)
410-
}, wait)
411-
412-
// For immediate return, try cached value or compute synchronously
413-
return memoized(...args)
414-
}
415-
}
416-
417377
/**
418378
* Memoize with WeakMap for object keys.
419379
* Allows garbage collection when objects are no longer referenced.

test/unit/memoization.test.mts

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* - memoize() caches synchronous function results
66
* - memoizeAsync() caches async function results with promise deduplication
77
* - memoizeWeak() uses WeakMap for object key caching
8-
* - memoizeDebounced() combines memoization with debouncing
98
* - once() ensures function executes exactly once
109
* - Memoize() decorator for class methods
1110
* - clearAllMemoizationCaches() global cache clearing
@@ -17,7 +16,6 @@ import {
1716
Memoize,
1817
memoize,
1918
memoizeAsync,
20-
memoizeDebounced,
2119
memoizeWeak,
2220
once,
2321
} from '@socketsecurity/lib/memoization'
@@ -542,66 +540,6 @@ describe('memoization', () => {
542540
})
543541
})
544542

545-
describe('memoizeDebounced', () => {
546-
it('should debounce and memoize function calls', async () => {
547-
const fn = vi.fn((n: number) => n * 2)
548-
const debounced = memoizeDebounced(fn, 100)
549-
550-
// First call - computes immediately
551-
expect(debounced(5)).toBe(10)
552-
expect(fn).toHaveBeenCalledTimes(1)
553-
554-
// Immediate second call - returns cached
555-
expect(debounced(5)).toBe(10)
556-
expect(fn).toHaveBeenCalledTimes(1)
557-
558-
// Wait for debounce
559-
await new Promise(resolve => setTimeout(resolve, 500))
560-
561-
// After debounce, still cached
562-
expect(debounced(5)).toBe(10)
563-
expect(fn).toHaveBeenCalledTimes(1)
564-
})
565-
566-
it('should clear debounce timeout on subsequent calls', async () => {
567-
const fn = vi.fn((n: number) => n * 2)
568-
const debounced = memoizeDebounced(fn, 100)
569-
570-
debounced(5)
571-
debounced(5)
572-
debounced(5)
573-
574-
expect(fn).toHaveBeenCalledTimes(1)
575-
576-
await new Promise(resolve => setTimeout(resolve, 500))
577-
578-
// Should still only be called once
579-
expect(fn).toHaveBeenCalledTimes(1)
580-
})
581-
582-
it('should handle different arguments', async () => {
583-
const fn = vi.fn((n: number) => n * 2)
584-
const debounced = memoizeDebounced(fn, 50)
585-
586-
expect(debounced(1)).toBe(2)
587-
expect(debounced(2)).toBe(4)
588-
expect(debounced(1)).toBe(2) // Cached
589-
590-
expect(fn).toHaveBeenCalledTimes(2)
591-
})
592-
593-
it('should respect memoization options', () => {
594-
const fn = vi.fn((n: number) => n * 2)
595-
const debounced = memoizeDebounced(fn, 50, { maxSize: 1 })
596-
597-
debounced(1)
598-
debounced(2) // Evicts 1
599-
debounced(1) // Cache miss
600-
601-
expect(fn).toHaveBeenCalledTimes(3)
602-
})
603-
})
604-
605543
describe('edge cases', () => {
606544
it('should handle very large cache sizes', () => {
607545
const fn = vi.fn((n: number) => n * 2)

0 commit comments

Comments
 (0)