chore: move fetch directory into shared/lib
This commit is contained in:
445
src/shared/lib/fetch/collectionCache.test.ts
Normal file
445
src/shared/lib/fetch/collectionCache.test.ts
Normal file
@@ -0,0 +1,445 @@
|
||||
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<typeof createCollectionCache<number>>;
|
||||
|
||||
beforeEach(() => {
|
||||
cache = createCollectionCache<number>();
|
||||
});
|
||||
|
||||
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<number>(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<ComplexType>();
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user