Initial commit of massive v2 rewrite

This commit is contained in:
Caileb 2025-08-02 14:26:52 -05:00
parent 1025f3b523
commit dc120fe78a
55 changed files with 21733 additions and 0 deletions

719
.tests/performance.test.js Normal file
View file

@ -0,0 +1,719 @@
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();
});
});
});