chore: delete unused code
This commit is contained in:
@@ -1,444 +0,0 @@
|
|||||||
import { get } from 'svelte/store';
|
|
||||||
import {
|
|
||||||
beforeEach,
|
|
||||||
describe,
|
|
||||||
expect,
|
|
||||||
it,
|
|
||||||
vi,
|
|
||||||
} from 'vitest';
|
|
||||||
import {
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
/**
|
|
||||||
* Collection cache manager
|
|
||||||
*
|
|
||||||
* Provides key-based caching, deduplication, and request tracking
|
|
||||||
* for any collection type. Integrates with Svelte stores for reactive updates.
|
|
||||||
*
|
|
||||||
* Key features:
|
|
||||||
* - Key-based caching (any ID, query hash)
|
|
||||||
* - Request deduplication (prevents concurrent requests for same key)
|
|
||||||
* - Request state tracking (fetching, ready, error)
|
|
||||||
* - TTL/staleness management
|
|
||||||
* - Performance timing tracking
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {
|
|
||||||
Readable,
|
|
||||||
Writable,
|
|
||||||
} from 'svelte/store';
|
|
||||||
import {
|
|
||||||
derived,
|
|
||||||
get,
|
|
||||||
writable,
|
|
||||||
} from 'svelte/store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal state for a cached item
|
|
||||||
* Tracks request lifecycle (fetching → ready/error)
|
|
||||||
*/
|
|
||||||
export interface CacheItemInternalState {
|
|
||||||
/** Whether a fetch is currently in progress */
|
|
||||||
fetching: boolean;
|
|
||||||
/** Whether data is ready and cached */
|
|
||||||
ready: boolean;
|
|
||||||
/** Error message if fetch failed */
|
|
||||||
error?: string;
|
|
||||||
/** Request start timestamp (performance tracking) */
|
|
||||||
startTime?: number;
|
|
||||||
/** Request end timestamp (performance tracking) */
|
|
||||||
endTime?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache configuration options
|
|
||||||
*/
|
|
||||||
export interface CacheOptions {
|
|
||||||
/** Default time-to-live for cached items (in milliseconds) */
|
|
||||||
defaultTTL?: number;
|
|
||||||
/** Maximum number of items to cache (LRU eviction) */
|
|
||||||
maxSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Statistics about cache performance
|
|
||||||
*/
|
|
||||||
export interface CacheStats {
|
|
||||||
/** Total number of items in cache */
|
|
||||||
total: number;
|
|
||||||
/** Number of items marked as ready */
|
|
||||||
cached: number;
|
|
||||||
/** Number of items currently fetching */
|
|
||||||
fetching: number;
|
|
||||||
/** Number of items with errors */
|
|
||||||
errors: number;
|
|
||||||
/** Total cache hits (data returned from cache) */
|
|
||||||
hits: number;
|
|
||||||
/** Total cache misses (data fetched from API) */
|
|
||||||
misses: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache manager interface
|
|
||||||
* Type-safe interface for collection caching operations
|
|
||||||
*/
|
|
||||||
export interface CollectionCacheManager<T> {
|
|
||||||
/** Get an item from cache by key */
|
|
||||||
get: (key: string) => T | undefined;
|
|
||||||
/** Check if item exists in cache and is ready */
|
|
||||||
has: (key: string) => boolean;
|
|
||||||
/** Check if item exists and is not stale */
|
|
||||||
hasFresh: (key: string) => boolean;
|
|
||||||
/** Set an item in cache (manual cache write) */
|
|
||||||
set: (key: string, value: T, ttl?: number) => void;
|
|
||||||
/** Remove item from cache */
|
|
||||||
remove: (key: string) => void;
|
|
||||||
/** Clear all items from cache */
|
|
||||||
clear: () => void;
|
|
||||||
/** Check if key is currently being fetched */
|
|
||||||
isFetching: (key: string) => boolean;
|
|
||||||
/** Get error for a key */
|
|
||||||
getError: (key: string) => string | undefined;
|
|
||||||
/** Get internal state for a key (for debugging) */
|
|
||||||
getInternalState: (key: string) => CacheItemInternalState | undefined;
|
|
||||||
/** Get cache statistics */
|
|
||||||
getStats: () => CacheStats;
|
|
||||||
/** Mark item as fetching (used when starting API request) */
|
|
||||||
markFetching: (key: string) => void;
|
|
||||||
/** Mark item as failed (used when API request fails) */
|
|
||||||
markFailed: (key: string, error: string) => void;
|
|
||||||
/** Increment cache miss counter */
|
|
||||||
markMiss: () => void;
|
|
||||||
/** Store containing cached data */
|
|
||||||
data: Writable<Record<string, T>>;
|
|
||||||
/** Store containing internal state (fetching, ready, error) */
|
|
||||||
internal: Writable<Record<string, CacheItemInternalState>>;
|
|
||||||
/** Derived store containing cache statistics */
|
|
||||||
stats: Readable<CacheStats>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a collection cache manager
|
|
||||||
*
|
|
||||||
* @typeParam T - Type of data being cached (e.g., UnifiedFont, Product, User)
|
|
||||||
* @param options - Cache configuration options
|
|
||||||
* @returns Cache manager instance
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const fontCache = createCollectionCache<UnifiedFont>({
|
|
||||||
* defaultTTL: 5 * 60 * 1000, // 5 minutes
|
|
||||||
* maxSize: 1000
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Set font in cache
|
|
||||||
* fontCache.set('Roboto', robotoFont);
|
|
||||||
*
|
|
||||||
* // Get font from cache
|
|
||||||
* const font = fontCache.get('Roboto');
|
|
||||||
* if (fontCache.hasFresh('Roboto')) {
|
|
||||||
* // Use cached font
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function createCollectionCache<T>(_options: CacheOptions = {}): CollectionCacheManager<T> {
|
|
||||||
// const { defaultTTL = 5 * 60 * 1000, maxSize = 1000 } = options;
|
|
||||||
|
|
||||||
// Stores for reactive data
|
|
||||||
const data: Writable<Record<string, T>> = writable({});
|
|
||||||
const internal: Writable<Record<string, CacheItemInternalState>> = writable({});
|
|
||||||
|
|
||||||
// Cache statistics store
|
|
||||||
const statsState = writable<CacheStats>({
|
|
||||||
total: 0,
|
|
||||||
cached: 0,
|
|
||||||
fetching: 0,
|
|
||||||
errors: 0,
|
|
||||||
hits: 0,
|
|
||||||
misses: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Derived stats store for reactive updates
|
|
||||||
const stats = derived([data, internal, statsState], ([$data, $internal, $statsState]) => ({
|
|
||||||
...$statsState,
|
|
||||||
total: Object.keys($data).length,
|
|
||||||
cached: Object.values($internal).filter(s => s.ready).length,
|
|
||||||
fetching: Object.values($internal).filter(s => s.fetching).length,
|
|
||||||
errors: Object.values($internal).filter(s => s.error).length,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Get cached data by key
|
|
||||||
* Returns undefined if not found
|
|
||||||
*/
|
|
||||||
get: (key: string) => {
|
|
||||||
const currentData = get(data);
|
|
||||||
return currentData[key];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if key exists in cache and is ready
|
|
||||||
*/
|
|
||||||
has: (key: string) => {
|
|
||||||
const currentInternal = get(internal);
|
|
||||||
const state = currentInternal[key];
|
|
||||||
return state?.ready === true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if key exists and is not stale (still within TTL)
|
|
||||||
*/
|
|
||||||
hasFresh: (key: string) => {
|
|
||||||
const currentInternal = get(internal);
|
|
||||||
const currentData = get(data);
|
|
||||||
|
|
||||||
const state = currentInternal[key];
|
|
||||||
if (!state?.ready) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if item exists in data store
|
|
||||||
if (!currentData[key]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement TTL check with cachedAt timestamps
|
|
||||||
// For now, just check ready state
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set data in cache
|
|
||||||
* Marks entry as ready and stops fetching state
|
|
||||||
*/
|
|
||||||
set: (key: string, value: T, _ttl?: number) => {
|
|
||||||
data.update(d => ({
|
|
||||||
...d,
|
|
||||||
[key]: value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
internal.update(i => {
|
|
||||||
const existingState = i[key];
|
|
||||||
return {
|
|
||||||
...i,
|
|
||||||
[key]: {
|
|
||||||
fetching: false,
|
|
||||||
ready: true,
|
|
||||||
error: undefined,
|
|
||||||
startTime: existingState?.startTime,
|
|
||||||
endTime: Date.now(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update statistics (cache hit)
|
|
||||||
statsState.update(s => ({ ...s, hits: s.hits + 1 }));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove item from cache
|
|
||||||
*/
|
|
||||||
remove: (key: string) => {
|
|
||||||
data.update(d => {
|
|
||||||
const { [key]: _, ...rest } = d;
|
|
||||||
return rest;
|
|
||||||
});
|
|
||||||
|
|
||||||
internal.update(i => {
|
|
||||||
const { [key]: _, ...rest } = i;
|
|
||||||
return rest;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all items from cache
|
|
||||||
*/
|
|
||||||
clear: () => {
|
|
||||||
data.set({});
|
|
||||||
internal.set({});
|
|
||||||
statsState.update(s => ({ ...s, hits: 0, misses: 0 }));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if key is currently being fetched
|
|
||||||
*/
|
|
||||||
isFetching: (key: string) => {
|
|
||||||
const currentInternal = get(internal);
|
|
||||||
return currentInternal[key]?.fetching === true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get error for a key
|
|
||||||
*/
|
|
||||||
getError: (key: string) => {
|
|
||||||
const currentInternal = get(internal);
|
|
||||||
return currentInternal[key]?.error;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get internal state for debugging
|
|
||||||
*/
|
|
||||||
getInternalState: (key: string) => {
|
|
||||||
const currentInternal = get(internal);
|
|
||||||
return currentInternal[key];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current cache statistics
|
|
||||||
*/
|
|
||||||
getStats: () => {
|
|
||||||
return get(stats);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark item as fetching (used when starting API request)
|
|
||||||
*/
|
|
||||||
markFetching: (key: string) => {
|
|
||||||
internal.update(internal => ({
|
|
||||||
...internal,
|
|
||||||
[key]: {
|
|
||||||
fetching: true,
|
|
||||||
ready: false,
|
|
||||||
error: undefined,
|
|
||||||
startTime: Date.now(),
|
|
||||||
endTime: undefined,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark item as failed (used when API request fails)
|
|
||||||
*/
|
|
||||||
markFailed: (key: string, error: string) => {
|
|
||||||
internal.update(internal => {
|
|
||||||
const existingState = internal[key];
|
|
||||||
return {
|
|
||||||
...internal,
|
|
||||||
[key]: {
|
|
||||||
fetching: false,
|
|
||||||
ready: false,
|
|
||||||
error,
|
|
||||||
startTime: existingState?.startTime,
|
|
||||||
endTime: Date.now(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update statistics
|
|
||||||
const currentStats = get(stats);
|
|
||||||
statsState.update(s => ({ ...s, errors: currentStats.errors + 1 }));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increment cache miss counter
|
|
||||||
*/
|
|
||||||
markMiss: () => {
|
|
||||||
statsState.update(s => ({ ...s, misses: s.misses + 1 }));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Expose stores for reactive binding
|
|
||||||
data,
|
|
||||||
internal,
|
|
||||||
stats,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared fetch layer exports
|
|
||||||
*
|
|
||||||
* Exports collection caching utilities and reactive patterns for Svelte 5
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { createCollectionCache } from './collectionCache';
|
|
||||||
export type {
|
|
||||||
CacheItemInternalState,
|
|
||||||
CacheOptions,
|
|
||||||
CacheStats,
|
|
||||||
CollectionCacheManager,
|
|
||||||
} from './collectionCache';
|
|
||||||
export { reactiveQueryArgs } from './reactiveQueryArgs';
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import type { Readable } from 'svelte/store';
|
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a reactive store that maintains stable references for query arguments
|
|
||||||
*
|
|
||||||
* This function wraps a callback in a Svelte store that updates via `$effect.pre()`,
|
|
||||||
* ensuring that the callback is called before DOM updates while maintaining object
|
|
||||||
* reference stability.
|
|
||||||
*
|
|
||||||
* @typeParam T - Type of query arguments (e.g., CreateQueryOptions)
|
|
||||||
* @param cb - Callback function that computes query arguments
|
|
||||||
* @returns Readable store containing current query arguments
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const queryArgsStore = reactiveQueryArgs(() => ({
|
|
||||||
* queryKey: ['fonts', search],
|
|
||||||
* queryFn: fetchFonts,
|
|
||||||
* staleTime: 5000
|
|
||||||
* }));
|
|
||||||
*
|
|
||||||
* // Use in component with TanStack Query
|
|
||||||
* const query = createQuery(queryArgsStore);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const reactiveQueryArgs = <T>(cb: () => T): Readable<T> => {
|
|
||||||
const store = writable<T>();
|
|
||||||
|
|
||||||
// Use $effect.pre() to run before DOM updates
|
|
||||||
// This ensures stable references while staying reactive
|
|
||||||
$effect.pre(() => {
|
|
||||||
store.set(cb());
|
|
||||||
});
|
|
||||||
|
|
||||||
return store;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user