import { mock, describe, expect, test, beforeEach } from 'bun:test' import { logMock } from '../../../tests/mocks/log' // Mock log.ts to cut the bootstrap/state dependency chain mock.module('src/utils/log.ts', logMock) const { memoizeWithTTL, memoizeWithTTLAsync, memoizeWithLRU } = await import( '../memoize' ) // ─── memoizeWithTTL ──────────────────────────────────────────────────── describe('memoizeWithTTL', () => { test('returns cached value on second call', () => { let calls = 0 const fn = memoizeWithTTL((x: number) => { calls++ return x * 2 }, 60_000) expect(fn(5)).toBe(10) expect(fn(5)).toBe(10) expect(calls).toBe(1) }) test('different args get separate cache entries', () => { let calls = 0 const fn = memoizeWithTTL((x: number) => { calls++ return x + 1 }, 60_000) expect(fn(1)).toBe(2) expect(fn(2)).toBe(3) expect(calls).toBe(2) }) test('cache.clear empties the cache', () => { let calls = 0 const fn = memoizeWithTTL(() => { calls++ return 'val' }, 60_000) fn() fn.cache.clear() fn() expect(calls).toBe(2) }) test('returns stale value and triggers background refresh after TTL', async () => { let calls = 0 const fn = memoizeWithTTL((x: number) => { calls++ return x * calls }, 1) // 1ms TTL const first = fn(10) expect(first).toBe(10) // calls=1, 10*1 // Wait for TTL to expire await new Promise(r => setTimeout(r, 10)) // Should return stale value (10) and trigger background refresh const second = fn(10) expect(second).toBe(10) // stale value returned immediately // Wait for background refresh microtask await new Promise(r => setTimeout(r, 10)) // Now cache should have refreshed value (calls=2 during refresh, 10*2=20) const third = fn(10) expect(third).toBe(20) }) }) // ─── memoizeWithTTLAsync ─────────────────────────────────────────────── describe('memoizeWithTTLAsync', () => { test('caches async result', async () => { let calls = 0 const fn = memoizeWithTTLAsync(async (x: number) => { calls++ return x * 2 }, 60_000) expect(await fn(5)).toBe(10) expect(await fn(5)).toBe(10) expect(calls).toBe(1) }) test('deduplicates concurrent cold-miss calls', async () => { let calls = 0 const fn = memoizeWithTTLAsync(async (x: number) => { calls++ await new Promise(r => setTimeout(r, 20)) return x }, 60_000) const [a, b, c] = await Promise.all([fn(1), fn(1), fn(1)]) expect(a).toBe(1) expect(b).toBe(1) expect(c).toBe(1) expect(calls).toBe(1) }) test('cache.clear forces re-computation', async () => { let calls = 0 const fn = memoizeWithTTLAsync(async () => { calls++ return 'v' }, 60_000) await fn() fn.cache.clear() await fn() expect(calls).toBe(2) }) test('returns stale value on TTL expiry', async () => { let calls = 0 const fn = memoizeWithTTLAsync(async () => { calls++ return calls }, 1) // 1ms TTL const first = await fn() expect(first).toBe(1) await new Promise(r => setTimeout(r, 10)) // Should return stale value (1) immediately const second = await fn() expect(second).toBe(1) }) }) // ─── memoizeWithLRU ──────────────────────────────────────────────────── describe('memoizeWithLRU', () => { test('caches results by key', () => { let calls = 0 const fn = memoizeWithLRU( (x: number) => { calls++ return x * 2 }, x => String(x), 10, ) expect(fn(5)).toBe(10) expect(fn(5)).toBe(10) expect(calls).toBe(1) }) test('evicts least recently used when max reached', () => { let calls = 0 const fn = memoizeWithLRU( (x: number) => { calls++ return x }, x => String(x), 3, ) fn(1) fn(2) fn(3) expect(calls).toBe(3) fn(4) // evicts key "1" expect(fn.cache.has('1')).toBe(false) expect(fn.cache.has('4')).toBe(true) }) test('cache.size returns current size', () => { const fn = memoizeWithLRU( (x: number) => x, x => String(x), 10, ) fn(1) fn(2) expect(fn.cache.size()).toBe(2) }) test('cache.delete removes entry', () => { const fn = memoizeWithLRU( (x: number) => x, x => String(x), 10, ) fn(1) expect(fn.cache.has('1')).toBe(true) fn.cache.delete('1') expect(fn.cache.has('1')).toBe(false) }) test('cache.get returns value without updating recency', () => { const fn = memoizeWithLRU( (x: number) => x * 10, x => String(x), 10, ) fn(5) expect(fn.cache.get('5')).toBe(50) }) test('cache.clear empties everything', () => { const fn = memoizeWithLRU( (x: number) => x, x => String(x), 10, ) fn(1) fn(2) fn.cache.clear() expect(fn.cache.size()).toBe(0) }) })