import { ErrorTaskPolicyType } from './define'; import { PromiseCacher } from './promise-cacher'; import { delay } from './util/delay'; describe('Memory Leak Prevention Tests', () => { let cacher: PromiseCacher; let mockFetchFn: jest.Mock; beforeEach(() => { jest.clearAllMocks(); mockFetchFn = jest.fn(); }); afterEach(() => { if (cacher) { cacher.clear(); cacher = undefined as any; } jest.restoreAllMocks(); }); describe('Timer Management', () => { it('should properly cleanup timer when cache is cleared', async () => { cacher = new PromiseCacher(mockFetchFn, { cachePolicy: { flushIntervalMs: 100 }, }); mockFetchFn.mockResolvedValue('test-value'); // Create some cache entries to ensure timer is active await cacher.get('key1'); await cacher.get('key2'); // Verify timer is running by checking internal state expect((cacher as any).timer).toBeTruthy(); // Clear cache should stop timer cacher.clear(); // Timer should be null after clear expect((cacher as any).timer).toBeNull(); }); it('should not create multiple timers', async () => { cacher = new PromiseCacher(mockFetchFn); mockFetchFn.mockResolvedValue('test'); // Multiple operations should not create multiple timers await cacher.get('key1'); const firstTimer = (cacher as any).timer; await cacher.get('key2'); const secondTimer = (cacher as any).timer; expect(firstTimer).toBe(secondTimer); }); it('should handle rapid clear operations without timer leaks', () => { cacher = new PromiseCacher(mockFetchFn); // Rapid clear operations for (let i = 0; i < 10; i++) { cacher.set(`key${i}`, `value${i}`); if (i % 2 === 0) { cacher.clear(); } } // Final clear to ensure clean state cacher.clear(); // Should end with clean state expect((cacher as any).timer).toBeNull(); expect(cacher.cacheCount).toBe(0); }); }); describe('Promise Reference Management', () => { it('should not hold references to rejected promises longer than necessary', async () => { cacher = new PromiseCacher(mockFetchFn, { cachePolicy: { errorTaskPolicy: ErrorTaskPolicyType.IGNORE }, }); const error = new Error('Test error'); mockFetchFn.mockRejectedValue(error); // This should fail and clean up immediately await expect(cacher.get('error-key')).rejects.toThrow('Test error'); // Wait for cleanup await new Promise((resolve) => setImmediate(resolve)); // Cache should be empty after error cleanup expect(cacher.cacheCount).toBe(0); }); it('should handle large number of concurrent promises without memory buildup', async () => { cacher = new PromiseCacher(mockFetchFn, { freeUpMemoryPolicy: { maxMemoryBytes: 1024 * 1024, // 1MB limit minMemoryBytes: 512 * 1024, // 512KB target }, cachePolicy: { flushIntervalMs: 50 }, }); mockFetchFn.mockImplementation(async (key: string) => { await delay(10); return 'x'.repeat(1000); // 1KB per item }); // Create many concurrent requests const promises = []; for (let i = 0; i < 2000; i++) { promises.push(cacher.get(`key${i}`)); } await Promise.all(promises); // Wait for memory cleanup to trigger await delay(200); const stats = cacher.statistics(); // Memory should be under control due to cleanup expect(stats.memory.currentUsageBytes).toBeLessThan(1024 * 1024 * 2); // Allow some overhead expect(stats.memory.cleanupCount).toBeGreaterThan(0); // Cleanup should have triggered }); }); describe('Metrics Array Growth Prevention', () => { it('should limit metrics array growth under high load', async () => { cacher = new PromiseCacher(mockFetchFn); mockFetchFn.mockImplementation(async () => { await delay(1); return 'value'; }); // Simulate high load const promises = []; for (let i = 0; i < 5000; i++) { promises.push(cacher.get(`burst-key-${i}`)); } await Promise.all(promises); const stats = cacher.statistics(); // Response times array should not grow excessively const responseTimes = (cacher as any).performanceMetrics.responseTimes; const recentResponseTimes = (cacher as any).performanceMetrics .recentResponseTimes; expect(responseTimes.length).toBeLessThanOrEqual(1000); // Max limit expect(recentResponseTimes.length).toBeLessThanOrEqual(100); // Max limit }); it('should handle burst scenarios in metrics collection', async () => { cacher = new PromiseCacher(mockFetchFn); let resolveCount = 0; mockFetchFn.mockImplementation(async () => { resolveCount++; return `value-${resolveCount}`; }); // Simulate burst load followed by normal load const burstPromises = []; for (let i = 0; i < 2000; i++) { burstPromises.push(cacher.get(`burst-${i}`)); } await Promise.all(burstPromises); // Check metrics arrays are still bounded const responseTimes = (cacher as any).performanceMetrics.responseTimes; expect(responseTimes.length).toBeLessThanOrEqual(1000); // Normal operations should still work const normalResult = await cacher.get('normal-key'); expect(normalResult).toBeDefined(); }); }); describe('Resource Cleanup on Errors', () => { it('should properly cleanup resources when fetch function throws', async () => { cacher = new PromiseCacher(mockFetchFn, { cachePolicy: { errorTaskPolicy: ErrorTaskPolicyType.IGNORE }, }); let errorCount = 0; mockFetchFn.mockImplementation(async () => { errorCount++; if (errorCount <= 5) { throw new Error(`Error ${errorCount}`); } return 'success'; }); // Multiple errors should not accumulate for (let i = 0; i < 5; i++) { await expect(cacher.get(`error-${i}`)).rejects.toThrow(); } // Wait for all error cleanups await new Promise((resolve) => setImmediate(resolve)); // Should be clean after errors expect(cacher.cacheCount).toBe(0); // Normal operation should still work const result = await cacher.get('success-key'); expect(result).toBe('success'); }); it('should handle mixed success and error scenarios without leaks', async () => { cacher = new PromiseCacher(mockFetchFn, { cachePolicy: { errorTaskPolicy: ErrorTaskPolicyType.IGNORE }, freeUpMemoryPolicy: { maxMemoryBytes: 10000, minMemoryBytes: 5000, }, }); mockFetchFn.mockImplementation(async (key: string) => { if (key.includes('error')) { throw new Error('Intentional error'); } return `success-${key}`; }); const promises = []; // Mix successful and failing requests for (let i = 0; i < 100; i++) { if (i % 3 === 0) { promises.push(cacher.get(`error-${i}`).catch(() => null)); } else { promises.push(cacher.get(`success-${i}`)); } } await Promise.all(promises); // Wait for error cleanup await new Promise((resolve) => setImmediate(resolve)); // Only success items should remain const stats = cacher.statistics(); expect(stats.inventory.totalItems).toBeGreaterThan(0); expect(stats.inventory.totalItems).toBeLessThan(100); // Some were errors and cleaned up }); }); describe('Long-running Instance Stability', () => { it('should maintain stable memory usage over extended operation', async () => { cacher = new PromiseCacher(mockFetchFn, { cachePolicy: { ttlMs: 1000, // Longer TTL to allow memory accumulation flushIntervalMs: 100, }, freeUpMemoryPolicy: { maxMemoryBytes: 5000, // Lower limit to trigger cleanup more easily minMemoryBytes: 2500, }, }); mockFetchFn.mockImplementation(async (key: string) => { await delay(1); return `data-${key}-${'x'.repeat(500)}`; // Larger items to increase memory usage }); // Simulate long-running operation with varying load for (let cycle = 0; cycle < 5; cycle++) { const cyclePromises = []; // Varying load per cycle const loadSize = 10 + (cycle % 3) * 5; for (let i = 0; i < loadSize; i++) { cyclePromises.push(cacher.get(`cycle-${cycle}-item-${i}`)); } await Promise.all(cyclePromises); // Wait for some TTL expiration and cleanup await delay(200); } const finalStats = cacher.statistics(); // Memory should be stable, not growing infinitely expect(finalStats.memory.currentUsageBytes).toBeLessThan(20000); expect(finalStats.memory.cleanupCount).toBeGreaterThan(0); }); }); });