cachly
Version:
Type-safe, production-ready in-memory cache system for Node.js and TypeScript with advanced features.
210 lines (168 loc) • 6.19 kB
text/typescript
import { Cachly } from '../Cachly';
describe('Cachly Basic', () => {
let cache: Cachly;
beforeEach(() => {
cache = new Cachly();
});
afterEach(() => {
cache.destroy();
});
describe('Basic Operations', () => {
it('should set and get values', async () => {
await cache.set('key1', 'value1');
const result = await cache.get('key1');
expect(result).toBe('value1');
});
it('should return undefined for non-existent keys', async () => {
const result = await cache.get('nonexistent');
expect(result).toBeUndefined();
});
it('should check if key exists', () => {
cache.set('key1', 'value1');
expect(cache.has('key1')).toBe(true);
expect(cache.has('nonexistent')).toBe(false);
});
it('should delete keys', async () => {
await cache.set('key1', 'value1');
expect(cache.has('key1')).toBe(true);
cache.delete('key1');
expect(cache.has('key1')).toBe(false);
});
it('should clear all items', async () => {
await cache.set('key1', 'value1');
await cache.set('key2', 'value2');
expect(cache.has('key1')).toBe(true);
expect(cache.has('key2')).toBe(true);
cache.clear();
expect(cache.has('key1')).toBe(false);
expect(cache.has('key2')).toBe(false);
});
});
describe('TTL', () => {
it('should expire items after TTL', async () => {
await cache.set('key1', 'value1', { ttl: 100 });
expect(await cache.get('key1')).toBe('value1');
await new Promise(resolve => setTimeout(resolve, 150));
expect(await cache.get('key1')).toBeUndefined();
});
it('should not expire items without TTL', async () => {
await cache.set('key1', 'value1');
await new Promise(resolve => setTimeout(resolve, 100));
expect(await cache.get('key1')).toBe('value1');
});
});
describe('Dependency Tracking', () => {
it('should invalidate dependents when dependency is deleted', async () => {
await cache.set('parent', 'parent-value');
await cache.set('child', 'child-value', { dependsOn: ['parent'] });
expect(cache.has('parent')).toBe(true);
expect(cache.has('child')).toBe(true);
cache.delete('parent');
expect(cache.has('parent')).toBe(false);
expect(cache.has('child')).toBe(false);
});
it('should handle multiple dependencies', async () => {
await cache.set('dep1', 'value1');
await cache.set('dep2', 'value2');
await cache.set('child', 'child-value', { dependsOn: ['dep1', 'dep2'] });
cache.delete('dep1');
expect(cache.has('child')).toBe(false);
});
});
describe('Async Operations', () => {
it('should handle getOrCompute', async () => {
let callCount = 0;
const loader = async () => {
callCount++;
return 'computed-value';
};
const result1 = await cache.getOrCompute('key1', loader);
const result2 = await cache.getOrCompute('key1', loader);
expect(result1).toBe('computed-value');
expect(result2).toBe('computed-value');
expect(callCount).toBe(1);
});
it('should handle getOrComputeWithStale', async () => {
let callCount = 0;
const loader = async () => {
callCount++;
return 'computed-value';
};
const result1 = await cache.getOrComputeWithStale('key1', loader);
const result2 = await cache.getOrComputeWithStale('key1', loader);
expect(result1).toBe('computed-value');
expect(result2).toBe('computed-value');
expect(callCount).toBe(1);
});
});
describe('Statistics', () => {
it('should track hits and misses', async () => {
await cache.set('key1', 'value1');
await cache.get('key1');
await cache.get('key1');
await cache.get('nonexistent');
const stats = cache.stats();
expect(stats.hits).toBe(2);
expect(stats.misses).toBe(1);
expect(stats.totalAccesses).toBe(3);
});
it('should calculate hit rate', async () => {
await cache.set('key1', 'value1');
await cache.get('key1');
await cache.get('key1');
await cache.get('nonexistent');
const stats = cache.stats();
expect(stats.hitRate).toBe((2 / 3) * 100);
expect(stats.missRate).toBe((1 / 3) * 100);
});
});
describe('Eviction', () => {
it('should evict items when maxItems is reached', async () => {
const limitedCache = new Cachly({ maxItems: 2 });
await limitedCache.set('key1', 'value1');
await limitedCache.set('key2', 'value2');
await limitedCache.set('key3', 'value3');
expect(limitedCache.has('key1')).toBe(false);
expect(limitedCache.has('key2')).toBe(true);
expect(limitedCache.has('key3')).toBe(true);
limitedCache.destroy();
});
});
describe('Events', () => {
it('should emit events', async () => {
const events: string[] = [];
cache.on('set', (key) => events.push(`set:${key}`));
cache.on('hit', (key) => events.push(`hit:${key}`));
cache.on('hit', (key) => events.push(`hit:${key}`));
cache.on('miss', (key) => events.push(`miss:${key}`));
cache.on('delete', (key) => events.push(`delete:${key}`));
await cache.set('key1', 'value1');
await cache.get('key1');
await cache.get('nonexistent');
cache.delete('key1');
expect(events).toContain('set:key1');
expect(events).toContain('hit:key1');
expect(events).toContain('miss:nonexistent');
expect(events).toContain('delete:key1');
});
});
describe('Warmup', () => {
it('should warm up cache with items', async () => {
const warmupItems = [
{
key: 'key1',
loader: async () => 'value1',
ttl: 1000,
},
{
key: 'key2',
loader: async () => 'value2',
ttl: 2000,
},
];
await cache.warm(warmupItems);
expect(await cache.get('key1')).toBe('value1');
expect(await cache.get('key2')).toBe('value2');
});
});
});