Skip to main content
Glama
InMemoryCacheProvider.test.ts7.72 kB
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { InMemoryCacheProvider } from './InMemoryCacheProvider'; describe('InMemoryCacheProvider', () => { let cache: InMemoryCacheProvider; beforeEach(() => { cache = new InMemoryCacheProvider(); vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); describe('constructor', () => { it('should create empty cache', () => { expect(cache.getSize()).toBe(0); }); it('should freeze instance', () => { expect(Object.isFrozen(cache)).toBe(true); }); }); describe('set() and get()', () => { it('should store and retrieve value', async () => { await cache.set('key1', 'value1', 60); const value = await cache.get<string>('key1'); expect(value).toBe('value1'); }); it('should store complex objects', async () => { const obj = { name: 'test', count: 42 }; await cache.set('obj1', obj, 60); const value = await cache.get<typeof obj>('obj1'); expect(value).toEqual(obj); }); it('should return undefined for non-existent key', async () => { const value = await cache.get('nonexistent'); expect(value).toBeUndefined(); }); it('should overwrite existing key', async () => { await cache.set('key1', 'value1', 60); await cache.set('key1', 'value2', 60); const value = await cache.get<string>('key1'); expect(value).toBe('value2'); }); }); describe('TTL expiration', () => { it('should expire after TTL', async () => { await cache.set('key1', 'value1', 10); // 10 seconds // Before expiration let value = await cache.get<string>('key1'); expect(value).toBe('value1'); // Advance time by 9 seconds (not expired yet) vi.advanceTimersByTime(9000); value = await cache.get<string>('key1'); expect(value).toBe('value1'); // Advance time by 2 more seconds (expired) vi.advanceTimersByTime(2000); value = await cache.get<string>('key1'); expect(value).toBeUndefined(); }); it('should never expire with TTL of 0', async () => { await cache.set('key1', 'value1', 0); // Advance time by 1 year vi.advanceTimersByTime(365 * 24 * 60 * 60 * 1000); const value = await cache.get<string>('key1'); expect(value).toBe('value1'); }); it('should remove expired entry from cache', async () => { await cache.set('key1', 'value1', 10); expect(cache.getSize()).toBe(1); // Expire the entry vi.advanceTimersByTime(11000); await cache.get('key1'); // Entry should be removed expect(cache.getSize()).toBe(0); }); it('should throw error for negative TTL', async () => { await expect(cache.set('key1', 'value1', -1)).rejects.toThrow('TTL cannot be negative'); }); }); describe('delete()', () => { it('should delete existing key', async () => { await cache.set('key1', 'value1', 60); await cache.delete('key1'); const value = await cache.get('key1'); expect(value).toBeUndefined(); }); it('should be idempotent for non-existent key', async () => { await cache.delete('nonexistent'); // Should not throw error }); it('should decrease cache size', async () => { await cache.set('key1', 'value1', 60); expect(cache.getSize()).toBe(1); await cache.delete('key1'); expect(cache.getSize()).toBe(0); }); }); describe('clear()', () => { it('should remove all entries', async () => { await cache.set('key1', 'value1', 60); await cache.set('key2', 'value2', 60); await cache.set('key3', 'value3', 60); expect(cache.getSize()).toBe(3); await cache.clear(); expect(cache.getSize()).toBe(0); }); it('should work on empty cache', async () => { await cache.clear(); expect(cache.getSize()).toBe(0); }); }); describe('has()', () => { it('should return true for existing key', async () => { await cache.set('key1', 'value1', 60); const exists = await cache.has('key1'); expect(exists).toBe(true); }); it('should return false for non-existent key', async () => { const exists = await cache.has('nonexistent'); expect(exists).toBe(false); }); it('should return false for expired key', async () => { await cache.set('key1', 'value1', 10); // Expire the entry vi.advanceTimersByTime(11000); const exists = await cache.has('key1'); expect(exists).toBe(false); }); }); describe('cleanup()', () => { it('should remove all expired entries', async () => { await cache.set('key1', 'value1', 10); // Expires in 10s await cache.set('key2', 'value2', 20); // Expires in 20s await cache.set('key3', 'value3', 0); // Never expires expect(cache.getSize()).toBe(3); // Advance time by 15 seconds (key1 expired, key2 not yet) vi.advanceTimersByTime(15000); cache.cleanup(); expect(cache.getSize()).toBe(2); // Verify correct keys remain expect(await cache.get('key1')).toBeUndefined(); expect(await cache.get('key2')).toBe('value2'); expect(await cache.get('key3')).toBe('value3'); }); it('should work on empty cache', () => { cache.cleanup(); expect(cache.getSize()).toBe(0); }); it('should not remove non-expired entries', async () => { await cache.set('key1', 'value1', 60); await cache.set('key2', 'value2', 60); cache.cleanup(); expect(cache.getSize()).toBe(2); }); }); describe('getSize()', () => { it('should return 0 for empty cache', () => { expect(cache.getSize()).toBe(0); }); it('should return correct count', async () => { await cache.set('key1', 'value1', 60); expect(cache.getSize()).toBe(1); await cache.set('key2', 'value2', 60); expect(cache.getSize()).toBe(2); }); it('should include expired entries until accessed or cleaned', async () => { await cache.set('key1', 'value1', 10); // Expire the entry vi.advanceTimersByTime(11000); // Size still includes expired entry expect(cache.getSize()).toBe(1); // Access triggers removal await cache.get('key1'); expect(cache.getSize()).toBe(0); }); }); describe('multiple concurrent operations', () => { it('should handle multiple keys independently', async () => { await cache.set('key1', 'value1', 10); await cache.set('key2', 'value2', 20); await cache.set('key3', 'value3', 30); // Advance to expire key1 only vi.advanceTimersByTime(11000); expect(await cache.get('key1')).toBeUndefined(); expect(await cache.get('key2')).toBe('value2'); expect(await cache.get('key3')).toBe('value3'); }); it('should handle mixed operations', async () => { await cache.set('key1', 'value1', 60); await cache.set('key2', 'value2', 60); expect(await cache.has('key1')).toBe(true); await cache.delete('key1'); expect(await cache.has('key1')).toBe(false); expect(await cache.has('key2')).toBe(true); await cache.clear(); expect(await cache.has('key2')).toBe(false); expect(cache.getSize()).toBe(0); }); }); describe('domain entity caching', () => { it('should cache frozen domain entities', async () => { const entity = Object.freeze({ id: '123', name: 'Test Entity', createdAt: new Date(), }); await cache.set('entity:123', entity, 60); const cached = await cache.get<typeof entity>('entity:123'); expect(cached).toEqual(entity); expect(Object.isFrozen(cached)).toBe(true); }); it('should cache arrays of entities', async () => { const entities = Object.freeze([ Object.freeze({ id: '1', name: 'Entity 1' }), Object.freeze({ id: '2', name: 'Entity 2' }), ]); await cache.set('entities:all', entities, 60); const cached = await cache.get<typeof entities>('entities:all'); expect(cached).toEqual(entities); expect(cached?.length).toBe(2); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/semanticintent/semantic-d1-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server