Massive v2 rewrite
This commit is contained in:
parent
1025f3b523
commit
5f1328f626
77 changed files with 28105 additions and 3542 deletions
719
.tests/performance.test.js
Normal file
719
.tests/performance.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue