import { jest } from '@jest/globals'; import { LRUCache, RateLimiter, ObjectPool, BatchProcessor, debounce, throttle, memoize, StringMatcher, ConnectionPool } from '../dist/utils/performance.js'; describe('Performance utilities', () => { describe('LRUCache', () => { test('should store and retrieve values', () => { const cache = new LRUCache(3); cache.set('key1', 'value1'); cache.set('key2', 'value2'); expect(cache.get('key1')).toBe('value1'); expect(cache.get('key2')).toBe('value2'); expect(cache.get('nonexistent')).toBeUndefined(); }); test('should evict least recently used items when at capacity', () => { const cache = new LRUCache(2); cache.set('key1', 'value1'); cache.set('key2', 'value2'); cache.set('key3', 'value3'); // Should evict key1 expect(cache.get('key1')).toBeUndefined(); expect(cache.get('key2')).toBe('value2'); expect(cache.get('key3')).toBe('value3'); }); test('should handle TTL expiration', () => { jest.useFakeTimers(); const cache = new LRUCache(10, 100); // 100ms TTL cache.set('key1', 'value1'); expect(cache.get('key1')).toBe('value1'); jest.advanceTimersByTime(150); expect(cache.get('key1')).toBeUndefined(); jest.useRealTimers(); }); test('should delete and clear items', () => { const cache = new LRUCache(5); cache.set('key1', 'value1'); cache.set('key2', 'value2'); expect(cache.delete('key1')).toBe(true); expect(cache.get('key1')).toBeUndefined(); cache.clear(); expect(cache.size).toBe(0); }); test('should clean up expired entries with cleanup method', () => { jest.useFakeTimers(); const cache = new LRUCache(10, 100); // 100ms TTL cache.set('key1', 'value1'); cache.set('key2', 'value2'); cache.set('key3', 'value3'); jest.advanceTimersByTime(150); const cleaned = cache.cleanup(); expect(cleaned).toBe(3); expect(cache.size).toBe(0); jest.useRealTimers(); }); test('should handle has() method with TTL expiration', () => { jest.useFakeTimers(); const cache = new LRUCache(10, 100); // 100ms TTL cache.set('key1', 'value1'); expect(cache.has('key1')).toBe(true); jest.advanceTimersByTime(150); expect(cache.has('key1')).toBe(false); jest.useRealTimers(); }); test('should cleanup without TTL should return 0', () => { const cache = new LRUCache(10); // No TTL cache.set('key1', 'value1'); cache.set('key2', 'value2'); const cleaned = cache.cleanup(); expect(cleaned).toBe(0); expect(cache.size).toBe(2); }); }); describe('RateLimiter', () => { let limiterInstances = []; afterEach(() => { // Clean up instances to prevent Jest hanging limiterInstances.forEach(limiter => { if (limiter && typeof limiter.destroy === 'function') { limiter.destroy(); } }); limiterInstances = []; }); test('should allow requests within limit', () => { const limiter = new RateLimiter(1000, 2); limiterInstances.push(limiter); expect(limiter.isAllowed('user1')).toBe(true); expect(limiter.isAllowed('user1')).toBe(true); expect(limiter.isAllowed('user1')).toBe(false); }); test('should reset after window expires', () => { jest.useFakeTimers(); const limiter = new RateLimiter(100, 2); limiterInstances.push(limiter); expect(limiter.isAllowed('user1')).toBe(true); expect(limiter.isAllowed('user1')).toBe(true); expect(limiter.isAllowed('user1')).toBe(false); jest.advanceTimersByTime(150); expect(limiter.isAllowed('user1')).toBe(true); }); test('should track different identifiers separately', () => { const limiter = new RateLimiter(1000, 2); limiterInstances.push(limiter); expect(limiter.isAllowed('user1')).toBe(true); expect(limiter.isAllowed('user1')).toBe(true); expect(limiter.isAllowed('user1')).toBe(false); expect(limiter.isAllowed('user2')).toBe(true); expect(limiter.isAllowed('user2')).toBe(true); expect(limiter.isAllowed('user2')).toBe(false); }); test('should clean up expired entries manually', () => { jest.useFakeTimers(); const limiter = new RateLimiter(100, 2); limiterInstances.push(limiter); limiter.isAllowed('user1'); limiter.isAllowed('user2'); limiter.isAllowed('user3'); jest.advanceTimersByTime(150); const cleaned = limiter.cleanup(); expect(cleaned).toBeGreaterThan(0); jest.useRealTimers(); }); test('should automatically clean up on interval', () => { jest.useFakeTimers(); const limiter = new RateLimiter(100, 2); limiterInstances.push(limiter); limiter.isAllowed('user1'); limiter.isAllowed('user2'); jest.advanceTimersByTime(150); // Trigger auto-cleanup (runs every 60 seconds) jest.advanceTimersByTime(60000); // Should still work after cleanup expect(limiter.isAllowed('user1')).toBe(true); jest.useRealTimers(); }); test('should handle cleanup of identifiers with partial expired requests', () => { jest.useFakeTimers(); const limiter = new RateLimiter(200, 3); limiterInstances.push(limiter); // Make some requests limiter.isAllowed('user1'); jest.advanceTimersByTime(100); limiter.isAllowed('user1'); jest.advanceTimersByTime(150); // First request now expired, second still valid const cleaned = limiter.cleanup(); expect(cleaned).toBe(0); // user1 still has valid requests, not removed // Advance further to expire all requests jest.advanceTimersByTime(100); const cleaned2 = limiter.cleanup(); expect(cleaned2).toBe(1); // user1 removed jest.useRealTimers(); }); }); describe('ObjectPool', () => { test('should create and reuse objects', () => { let created = 0; const factory = () => ({ id: ++created }); const reset = (obj) => { obj.used = false; }; const pool = new ObjectPool(factory, reset); const obj1 = pool.acquire(); expect(obj1.id).toBe(1); pool.release(obj1); const obj2 = pool.acquire(); expect(obj2.id).toBe(1); // Reused expect(obj2.used).toBe(false); // Reset was called }); test('should create new objects when pool is empty', () => { let created = 0; const factory = () => ({ id: ++created }); const reset = () => {}; const pool = new ObjectPool(factory, reset); const obj1 = pool.acquire(); const obj2 = pool.acquire(); expect(obj1.id).toBe(1); expect(obj2.id).toBe(2); }); test('should not exceed max size', () => { const factory = () => ({}); const reset = () => {}; const pool = new ObjectPool(factory, reset, 2); const obj1 = pool.acquire(); const obj2 = pool.acquire(); const obj3 = pool.acquire(); pool.release(obj1); pool.release(obj2); pool.release(obj3); const stats = pool.size; expect(stats.available).toBeLessThanOrEqual(2); }); test('should ignore release of objects not in use', () => { const factory = () => ({ id: Math.random() }); const reset = () => {}; const pool = new ObjectPool(factory, reset); const strangerObj = factory(); // Should not throw or affect pool pool.release(strangerObj); expect(pool.size.available).toBe(0); expect(pool.size.inUse).toBe(0); }); test('should clear all objects from pool', () => { const factory = () => ({ id: Math.random() }); const reset = () => {}; const pool = new ObjectPool(factory, reset); const obj1 = pool.acquire(); const obj2 = pool.acquire(); pool.release(obj1); pool.clear(); const stats = pool.size; expect(stats.available).toBe(0); expect(stats.inUse).toBe(0); expect(stats.total).toBe(0); }); test('should provide accurate size statistics', () => { const factory = () => ({ id: Math.random() }); const reset = () => {}; const pool = new ObjectPool(factory, reset); const obj1 = pool.acquire(); const obj2 = pool.acquire(); pool.release(obj1); const stats = pool.size; expect(stats.available).toBe(1); expect(stats.inUse).toBe(1); expect(stats.total).toBe(2); }); }); describe('BatchProcessor', () => { let batcherInstances = []; beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); // Clean up any batcher instances to prevent memory leaks batcherInstances.forEach(batcher => { if (batcher && typeof batcher.destroy === 'function') { batcher.destroy(); } }); batcherInstances = []; }); test('should process batch when size is reached', async () => { const processor = jest.fn(); const batcher = new BatchProcessor(processor, { batchSize: 3 }); batcherInstances.push(batcher); batcher.add('item1'); batcher.add('item2'); expect(processor).not.toHaveBeenCalled(); batcher.add('item3'); await Promise.resolve(); // Let async processing complete expect(processor).toHaveBeenCalledWith(['item1', 'item2', 'item3']); }); test('should auto-flush on interval', async () => { const processor = jest.fn(); const batcher = new BatchProcessor(processor, { batchSize: 10, flushInterval: 100 }); batcherInstances.push(batcher); batcher.add('item1'); batcher.add('item2'); jest.advanceTimersByTime(100); await Promise.resolve(); expect(processor).toHaveBeenCalledWith(['item1', 'item2']); }); test('should handle processing errors', async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); const processor = jest.fn().mockRejectedValue(new Error('Process error')); const batcher = new BatchProcessor(processor, { batchSize: 1 }); batcherInstances.push(batcher); batcher.add('item1'); await Promise.resolve(); expect(consoleErrorSpy).toHaveBeenCalledWith('Batch processing error:', expect.any(Error)); consoleErrorSpy.mockRestore(); }); test('should not flush when already processing', async () => { let resolveProcessor; const processor = jest.fn(() => new Promise(resolve => { resolveProcessor = resolve; })); const batcher = new BatchProcessor(processor, { batchSize: 2 }); batcherInstances.push(batcher); batcher.add('item1'); batcher.add('item2'); // Triggers flush // Add more items while first batch is processing batcher.add('item3'); batcher.flush(); // Should return early expect(processor).toHaveBeenCalledTimes(1); // Resolve the first batch resolveProcessor(); await Promise.resolve(); expect(processor).toHaveBeenCalledWith(['item1', 'item2']); }); test('should not flush empty queue', async () => { const processor = jest.fn(); const batcher = new BatchProcessor(processor, { batchSize: 5 }); batcherInstances.push(batcher); await batcher.flush(); expect(processor).not.toHaveBeenCalled(); }); }); describe('debounce', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); test('should delay function execution', () => { const func = jest.fn(); const debounced = debounce(func, 100); debounced('arg1'); debounced('arg2'); debounced('arg3'); expect(func).not.toHaveBeenCalled(); jest.advanceTimersByTime(100); expect(func).toHaveBeenCalledTimes(1); expect(func).toHaveBeenCalledWith('arg3'); }); }); describe('throttle', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); test('should limit function execution rate', () => { const func = jest.fn(); const throttled = throttle(func, 100); throttled('arg1'); throttled('arg2'); throttled('arg3'); expect(func).toHaveBeenCalledTimes(1); expect(func).toHaveBeenCalledWith('arg1'); jest.advanceTimersByTime(100); throttled('arg4'); expect(func).toHaveBeenCalledTimes(2); expect(func).toHaveBeenCalledWith('arg4'); }); }); describe('memoize', () => { test('should cache function results', () => { const func = jest.fn((a, b) => a + b); const memoized = memoize(func); expect(memoized(1, 2)).toBe(3); expect(memoized(1, 2)).toBe(3); expect(memoized(1, 2)).toBe(3); expect(func).toHaveBeenCalledTimes(1); }); test('should handle different arguments', () => { const func = jest.fn((a, b) => a + b); const memoized = memoize(func); expect(memoized(1, 2)).toBe(3); expect(memoized(2, 3)).toBe(5); expect(func).toHaveBeenCalledTimes(2); }); test('should respect TTL option', async () => { jest.useFakeTimers(); const func = jest.fn((a) => a * 2); const memoized = memoize(func, { ttl: 100 }); expect(memoized(5)).toBe(10); expect(func).toHaveBeenCalledTimes(1); expect(memoized(5)).toBe(10); expect(func).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(150); expect(memoized(5)).toBe(10); expect(func).toHaveBeenCalledTimes(2); jest.useRealTimers(); }); test('should handle undefined cached values correctly', () => { const func = jest.fn(() => undefined); const memoized = memoize(func); expect(memoized('test')).toBeUndefined(); expect(memoized('test')).toBeUndefined(); // Function should be called twice since undefined is returned expect(func).toHaveBeenCalledTimes(2); }); test('should handle functions returning falsy values', () => { const func = jest.fn((x) => x === 'zero' ? 0 : x === 'false' ? false : x === 'null' ? null : 'default'); const memoized = memoize(func); expect(memoized('zero')).toBe(0); expect(memoized('zero')).toBe(0); // Should be cached expect(func).toHaveBeenCalledTimes(1); expect(memoized('false')).toBe(false); expect(memoized('false')).toBe(false); // Should be cached expect(func).toHaveBeenCalledTimes(2); expect(memoized('null')).toBe(null); expect(memoized('null')).toBe(null); // Should be cached expect(func).toHaveBeenCalledTimes(3); }); }); describe('StringMatcher', () => { test('should match strings case-insensitively', () => { const matcher = new StringMatcher(['apple', 'BANANA', 'Cherry']); expect(matcher.contains('apple')).toBe(true); expect(matcher.contains('APPLE')).toBe(true); expect(matcher.contains('banana')).toBe(true); expect(matcher.contains('cherry')).toBe(true); expect(matcher.contains('grape')).toBe(false); }); test('should add and remove patterns', () => { const matcher = new StringMatcher(['apple']); matcher.add('banana'); expect(matcher.contains('banana')).toBe(true); expect(matcher.size).toBe(2); expect(matcher.remove('apple')).toBe(true); expect(matcher.contains('apple')).toBe(false); expect(matcher.size).toBe(1); }); test('should check if any text matches with containsAny', () => { const matcher = new StringMatcher(['apple', 'banana', 'cherry']); expect(matcher.containsAny(['grape', 'orange'])).toBe(false); expect(matcher.containsAny(['grape', 'apple'])).toBe(true); expect(matcher.containsAny(['BANANA', 'orange'])).toBe(true); expect(matcher.containsAny([])).toBe(false); expect(matcher.containsAny(['cherry', 'apple', 'banana'])).toBe(true); }); test('should handle empty patterns', () => { const matcher = new StringMatcher([]); expect(matcher.contains('anything')).toBe(false); expect(matcher.containsAny(['test', 'values'])).toBe(false); expect(matcher.size).toBe(0); }); test('should remove non-existent patterns gracefully', () => { const matcher = new StringMatcher(['apple']); expect(matcher.remove('banana')).toBe(false); expect(matcher.size).toBe(1); }); }); describe('ConnectionPool', () => { test('should create and reuse connections', () => { const pool = new ConnectionPool({ maxConnections: 5 }); const conn1 = pool.getConnection('host1'); expect(conn1).not.toBeNull(); expect(conn1.host).toBe('host1'); pool.releaseConnection('host1', conn1); const conn2 = pool.getConnection('host1'); expect(conn2).toBe(conn1); // Reused }); test('should respect max connections limit', () => { const pool = new ConnectionPool({ maxConnections: 2 }); const conn1 = pool.getConnection('host1'); const conn2 = pool.getConnection('host1'); const conn3 = pool.getConnection('host1'); expect(conn1).not.toBeNull(); expect(conn2).not.toBeNull(); expect(conn3).toBeNull(); // Pool exhausted }); test('should create separate pools for different hosts', () => { const pool = new ConnectionPool({ maxConnections: 2 }); const conn1 = pool.getConnection('host1'); const conn2 = pool.getConnection('host2'); expect(conn1).not.toBeNull(); expect(conn2).not.toBeNull(); expect(conn1.host).toBe('host1'); expect(conn2.host).toBe('host2'); }); test('should handle release of non-existent connections gracefully', () => { const pool = new ConnectionPool({ maxConnections: 5 }); const fakeConn = { host: 'fake', created: Date.now() }; // Should not throw pool.releaseConnection('host1', fakeConn); pool.releaseConnection('nonexistent', fakeConn); }); test('should close connections when pool is over half capacity', () => { const pool = new ConnectionPool({ maxConnections: 4 }); // Spy on closeConnection method const closeConnectionSpy = jest.spyOn(pool, 'closeConnection'); // Fill pool const conn1 = pool.getConnection('host1'); const conn2 = pool.getConnection('host1'); const conn3 = pool.getConnection('host1'); // Release connections pool.releaseConnection('host1', conn1); // Should keep (pool size 1) pool.releaseConnection('host1', conn2); // Should keep (pool size 2 = maxConnections/2) pool.releaseConnection('host1', conn3); // Should close (pool would exceed half capacity) expect(closeConnectionSpy).toHaveBeenCalledTimes(1); expect(closeConnectionSpy).toHaveBeenCalledWith(conn3); closeConnectionSpy.mockRestore(); }); test('should provide access to connectionTimeout property', () => { const pool = new ConnectionPool({ timeout: 5000 }); expect(pool.connectionTimeout).toBe(5000); }); test('should destroy all connections when destroyed', () => { const pool = new ConnectionPool({ maxConnections: 3 }); // Spy on closeConnection method const closeConnectionSpy = jest.spyOn(pool, 'closeConnection'); // Create some connections const conn1 = pool.getConnection('host1'); const conn2 = pool.getConnection('host1'); const conn3 = pool.getConnection('host2'); // Release one connection back to pool pool.releaseConnection('host1', conn1); // Destroy pool pool.destroy(); // Should close all connections (1 in pool + 2 in use) expect(closeConnectionSpy).toHaveBeenCalledTimes(3); expect(closeConnectionSpy).toHaveBeenCalledWith(conn1); expect(closeConnectionSpy).toHaveBeenCalledWith(conn2); expect(closeConnectionSpy).toHaveBeenCalledWith(conn3); closeConnectionSpy.mockRestore(); }); test('should create connections with timestamp', () => { const pool = new ConnectionPool(); const before = Date.now(); const conn = pool.getConnection('host1'); const after = Date.now(); expect(conn.created).toBeGreaterThanOrEqual(before); expect(conn.created).toBeLessThanOrEqual(after); }); test('should use default maxConnections and timeout values', () => { const pool = new ConnectionPool(); expect(pool.connectionTimeout).toBe(30000); // Default timeout // Create connections up to default limit (50) const connections = []; for (let i = 0; i < 50; i++) { const conn = pool.getConnection('host1'); if (conn) connections.push(conn); } expect(connections).toHaveLength(50); // 51st connection should be null const extraConn = pool.getConnection('host1'); expect(extraConn).toBeNull(); }); }); });