import { DebouncedCache } from '../debounced-cache'; describe('DebouncedCache', () => { beforeAll(() => { vi.useFakeTimers(); }); const consoleSpy = { debug: vi.spyOn(console, 'debug'), error: vi.spyOn(console, 'error'), }; afterEach(() => { consoleSpy.debug.mockClear(); consoleSpy.error.mockClear(); }); it('should call the function after a debounce threshold and cache the result', async () => { const { producer, cache } = createTestCache(); const consumer = vi.fn(); cache.schedule({ a: 1, b: 2 }, consumer); await vi.runAllTimersAsync(); expect(producer).toHaveBeenCalledWith({ a: 1, b: 2 }); expect(consumer).toHaveBeenCalledWith(3); cache.schedule({ a: 1, b: 2 }, consumer); await vi.runAllTimersAsync(); expect(producer).toHaveBeenCalledTimes(1); expect(consumer).toHaveBeenCalledTimes(2); expect(consoleSpy.debug).not.toHaveBeenCalled(); expect(consoleSpy.error).not.toHaveBeenCalled(); }); describe('should abort the last scheduled call when a new one is scheduled', () => { test('scheduling before the debounce threshold is reached', async () => { const { producer, cache } = createTestCache(); const consumer1 = vi.fn(); const consumer2 = vi.fn(); cache.schedule({ a: 1, b: 2 }, consumer1); cache.schedule({ a: 1, b: 2 }, consumer2); await vi.runAllTimersAsync(); expect(consumer1).not.toHaveBeenCalled(); expect(consumer2).toHaveBeenCalledWith(3); expect(producer).toHaveBeenCalledOnce(); // No logs should be emitted because the `setTimeout` call itself should have been aborted. expect(consoleSpy.debug.mock.calls).toMatchInlineSnapshot(`[]`); expect(consoleSpy.error.mock.calls).toMatchInlineSnapshot(`[]`); }); test('scheduling after the debounce threshold is reached', async () => { const threshold = 300; const { producer, cache } = createTestCache(threshold); const consumer1 = vi.fn(); const consumer2 = vi.fn(); cache.schedule({ a: 1, b: 2 }, consumer1); vi.advanceTimersByTime(threshold); cache.schedule({ a: 1, b: 2 }, consumer2); await vi.runAllTimersAsync(); expect(consumer1).not.toHaveBeenCalled(); expect(consumer2).toHaveBeenCalledWith(3); expect(producer).toHaveBeenCalledOnce(); expect(consoleSpy.debug.mock.calls).toMatchInlineSnapshot(` [ [ "A call was aborted after the debounce threshold was reached", DOMException {}, ], ] `); expect(consoleSpy.error.mock.calls).toMatchInlineSnapshot(`[]`); }); }); describe('should handle errors by logging them', () => { test('error in producer', async () => { const producer = vi.fn(() => Promise.reject(new Error('producer error'))); const cache = new DebouncedCache(producer); const consumer = vi.fn(); cache.schedule(undefined, consumer); await vi.runAllTimersAsync(); expect(consumer).not.toHaveBeenCalled(); expect(consoleSpy.debug).not.toHaveBeenCalled(); expect(consoleSpy.error.mock.calls).toMatchInlineSnapshot(` [ [ "An error occurred while calling 'spy'.", [Error: producer error], ], ] `); }); test('error in consumer', async () => { const { producer, cache } = createTestCache(); const consumer = vi.fn(() => { throw new Error('consumer error'); }); cache.schedule({ a: 1, b: 2 }, consumer); await vi.runAllTimersAsync(); expect(producer).toHaveBeenCalledOnce(); expect(consoleSpy.debug).not.toHaveBeenCalled(); expect(consoleSpy.error.mock.calls).toMatchInlineSnapshot(` [ [ "An error occurred while processing the result of 'producerImpl'.", [Error: consumer error], ], ] `); }); }); }); function createTestCache(thresholdMs?: number) { const producer = vi.fn(producerImpl); const cache = new DebouncedCache(producer, { thresholdMs }); return { producer, cache }; } interface ProducerParams { a: number; b: number; } async function producerImpl(params: ProducerParams): Promise { return params.a + params.b; }