import { get } from 'svelte/store'; import { beforeEach, describe, expect, it, vi, } from 'vitest'; import { type CacheItemInternalState, type CacheOptions, createCollectionCache, } from './collectionCache'; describe('createCollectionCache', () => { let cache: ReturnType>; beforeEach(() => { cache = createCollectionCache(); }); describe('initialization', () => { it('initializes with empty cache', () => { const data = get(cache.data); expect(data).toEqual({}); }); it('initializes with default options', () => { const stats = cache.getStats(); expect(stats.total).toBe(0); expect(stats.cached).toBe(0); expect(stats.fetching).toBe(0); expect(stats.errors).toBe(0); expect(stats.hits).toBe(0); expect(stats.misses).toBe(0); }); it('accepts custom cache options', () => { const options: CacheOptions = { defaultTTL: 10 * 60 * 1000, // 10 minutes maxSize: 500, }; const customCache = createCollectionCache(options); expect(customCache).toBeDefined(); }); }); describe('set and get', () => { it('sets a value in cache', () => { cache.set('key1', 100); const value = cache.get('key1'); expect(value).toBe(100); }); it('sets multiple values in cache', () => { cache.set('key1', 100); cache.set('key2', 200); cache.set('key3', 300); expect(cache.get('key1')).toBe(100); expect(cache.get('key2')).toBe(200); expect(cache.get('key3')).toBe(300); }); it('updates existing value', () => { cache.set('key1', 100); cache.set('key1', 150); expect(cache.get('key1')).toBe(150); }); it('returns undefined for non-existent key', () => { const value = cache.get('non-existent'); expect(value).toBeUndefined(); }); it('marks item as ready after set', () => { cache.set('key1', 100); const internalState = cache.getInternalState('key1'); expect(internalState?.ready).toBe(true); expect(internalState?.fetching).toBe(false); }); }); describe('has and hasFresh', () => { it('returns false for non-existent key', () => { expect(cache.has('non-existent')).toBe(false); expect(cache.hasFresh('non-existent')).toBe(false); }); it('returns true after setting value', () => { cache.set('key1', 100); expect(cache.has('key1')).toBe(true); expect(cache.hasFresh('key1')).toBe(true); }); it('returns false for fetching items', () => { cache.markFetching('key1'); expect(cache.has('key1')).toBe(false); expect(cache.hasFresh('key1')).toBe(false); }); it('returns false for failed items', () => { cache.markFailed('key1', 'Network error'); expect(cache.has('key1')).toBe(false); expect(cache.hasFresh('key1')).toBe(false); }); }); describe('remove', () => { it('removes a value from cache', () => { cache.set('key1', 100); cache.set('key2', 200); cache.remove('key1'); expect(cache.get('key1')).toBeUndefined(); expect(cache.get('key2')).toBe(200); }); it('removes internal state', () => { cache.set('key1', 100); cache.remove('key1'); const state = cache.getInternalState('key1'); expect(state).toBeUndefined(); }); it('does nothing for non-existent key', () => { expect(() => cache.remove('non-existent')).not.toThrow(); }); }); describe('clear', () => { it('clears all values from cache', () => { cache.set('key1', 100); cache.set('key2', 200); cache.set('key3', 300); cache.clear(); expect(cache.get('key1')).toBeUndefined(); expect(cache.get('key2')).toBeUndefined(); expect(cache.get('key3')).toBeUndefined(); }); it('clears internal state', () => { cache.set('key1', 100); cache.clear(); const state = cache.getInternalState('key1'); expect(state).toBeUndefined(); }); it('resets cache statistics', () => { cache.set('key1', 100); // This increments hits const statsBefore = cache.getStats(); cache.clear(); const statsAfter = cache.getStats(); expect(statsAfter.hits).toBe(0); expect(statsAfter.misses).toBe(0); }); }); describe('markFetching', () => { it('marks item as fetching', () => { cache.markFetching('key1'); expect(cache.isFetching('key1')).toBe(true); const state = cache.getInternalState('key1'); expect(state?.fetching).toBe(true); expect(state?.ready).toBe(false); expect(state?.startTime).toBeDefined(); }); it('updates existing state when called again', () => { cache.markFetching('key1'); const startTime1 = cache.getInternalState('key1')?.startTime; // Wait a bit to ensure different timestamp vi.useFakeTimers(); vi.advanceTimersByTime(100); cache.markFetching('key1'); const startTime2 = cache.getInternalState('key1')?.startTime; expect(startTime2).toBeGreaterThan(startTime1!); vi.useRealTimers(); }); it('sets endTime to undefined', () => { cache.markFetching('key1'); const state = cache.getInternalState('key1'); expect(state?.endTime).toBeUndefined(); }); }); describe('markFailed', () => { it('marks item as failed with error message', () => { cache.markFailed('key1', 'Network error'); expect(cache.isFetching('key1')).toBe(false); const error = cache.getError('key1'); expect(error).toBe('Network error'); const state = cache.getInternalState('key1'); expect(state?.fetching).toBe(false); expect(state?.ready).toBe(false); expect(state?.error).toBe('Network error'); }); it('preserves start time from fetching state', () => { cache.markFetching('key1'); const startTime = cache.getInternalState('key1')?.startTime; cache.markFailed('key1', 'Error'); const state = cache.getInternalState('key1'); expect(state?.startTime).toBe(startTime); }); it('sets end time', () => { cache.markFailed('key1', 'Error'); const state = cache.getInternalState('key1'); expect(state?.endTime).toBeDefined(); }); it('increments error counter', () => { const statsBefore = cache.getStats(); cache.markFailed('key1', 'Error1'); const statsAfter1 = cache.getStats(); expect(statsAfter1.errors).toBe(statsBefore.errors + 1); cache.markFailed('key2', 'Error2'); const statsAfter2 = cache.getStats(); expect(statsAfter2.errors).toBe(statsAfter1.errors + 1); }); }); describe('markMiss', () => { it('increments miss counter', () => { const statsBefore = cache.getStats(); cache.markMiss(); const statsAfter = cache.getStats(); expect(statsAfter.misses).toBe(statsBefore.misses + 1); }); it('increments miss counter multiple times', () => { const statsBefore = cache.getStats(); cache.markMiss(); cache.markMiss(); cache.markMiss(); const statsAfter = cache.getStats(); expect(statsAfter.misses).toBe(statsBefore.misses + 3); }); }); describe('statistics', () => { it('tracks total number of items', () => { expect(cache.getStats().total).toBe(0); cache.set('key1', 100); expect(cache.getStats().total).toBe(1); cache.set('key2', 200); expect(cache.getStats().total).toBe(2); cache.remove('key1'); expect(cache.getStats().total).toBe(1); }); it('tracks number of cached (ready) items', () => { expect(cache.getStats().cached).toBe(0); cache.set('key1', 100); expect(cache.getStats().cached).toBe(1); cache.set('key2', 200); expect(cache.getStats().cached).toBe(2); cache.markFetching('key3'); expect(cache.getStats().cached).toBe(2); }); it('tracks number of fetching items', () => { expect(cache.getStats().fetching).toBe(0); cache.markFetching('key1'); expect(cache.getStats().fetching).toBe(1); cache.markFetching('key2'); expect(cache.getStats().fetching).toBe(2); cache.set('key1', 100); expect(cache.getStats().fetching).toBe(1); }); it('tracks cache hits', () => { const statsBefore = cache.getStats(); cache.set('key1', 100); const statsAfter1 = cache.getStats(); expect(statsAfter1.hits).toBe(statsBefore.hits + 1); cache.set('key2', 200); const statsAfter2 = cache.getStats(); expect(statsAfter2.hits).toBe(statsAfter1.hits + 1); }); it('provides derived stats store', () => { cache.set('key1', 100); cache.markFetching('key2'); const stats = get(cache.stats); expect(stats.total).toBe(1); expect(stats.cached).toBe(1); expect(stats.fetching).toBe(1); }); }); describe('store reactivity', () => { it('updates data store reactively', () => { let dataUpdates = 0; const unsubscribe = cache.data.subscribe(() => { dataUpdates++; }); cache.set('key1', 100); cache.set('key2', 200); expect(dataUpdates).toBeGreaterThan(0); unsubscribe(); }); it('updates internal state store reactively', () => { let internalUpdates = 0; const unsubscribe = cache.internal.subscribe(() => { internalUpdates++; }); cache.markFetching('key1'); cache.set('key1', 100); cache.markFailed('key2', 'Error'); expect(internalUpdates).toBeGreaterThan(0); unsubscribe(); }); it('updates stats store reactively', () => { let statsUpdates = 0; const unsubscribe = cache.stats.subscribe(() => { statsUpdates++; }); cache.set('key1', 100); cache.markMiss(); expect(statsUpdates).toBeGreaterThan(0); unsubscribe(); }); }); describe('edge cases', () => { it('handles complex types', () => { interface ComplexType { id: string; value: number; tags: string[]; } const complexCache = createCollectionCache(); const item: ComplexType = { id: '1', value: 42, tags: ['a', 'b', 'c'], }; complexCache.set('item1', item); const retrieved = complexCache.get('item1'); expect(retrieved).toEqual(item); expect(retrieved?.tags).toEqual(['a', 'b', 'c']); }); it('handles special characters in keys', () => { cache.set('key with spaces', 1); cache.set('key/with/slashes', 2); cache.set('key-with-dashes', 3); expect(cache.get('key with spaces')).toBe(1); expect(cache.get('key/with/slashes')).toBe(2); expect(cache.get('key-with-dashes')).toBe(3); }); it('handles rapid set and remove operations', () => { for (let i = 0; i < 100; i++) { cache.set(`key${i}`, i); } for (let i = 0; i < 100; i += 2) { cache.remove(`key${i}`); } expect(cache.getStats().total).toBe(50); expect(cache.get('key0')).toBeUndefined(); expect(cache.get('key1')).toBe(1); }); }); describe('error handling', () => { it('handles concurrent markFetching for same key', () => { cache.markFetching('key1'); cache.markFetching('key1'); const state = cache.getInternalState('key1'); expect(state?.fetching).toBe(true); expect(state?.startTime).toBeDefined(); }); it('handles marking failed without prior fetching', () => { cache.markFailed('key1', 'Error'); const state = cache.getInternalState('key1'); expect(state?.fetching).toBe(false); expect(state?.ready).toBe(false); expect(state?.error).toBe('Error'); }); it('handles operations on removed keys', () => { cache.set('key1', 100); cache.remove('key1'); expect(() => cache.set('key1', 200)).not.toThrow(); expect(() => cache.remove('key1')).not.toThrow(); expect(() => cache.getError('key1')).not.toThrow(); }); }); });