mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
- Add Disposable, BufferConfig, MemoryMetrics, CleanupRegistration types - Create src/config/buffer-limits.ts with consolidated buffer size constants - Create src/config/map-limits.ts with Map size limits to prevent unbounded growth - Implement BufferAccumulator utility with configurable trim and onTrim callback - Implement LRUMap with automatic eviction and O(1) operations - Implement CleanupManager for unified resource cleanup with isStopped guard - Add comprehensive tests for all new utilities This lays the foundation for memory leak prevention and performance improvements. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
302 lines
8.1 KiB
TypeScript
302 lines
8.1 KiB
TypeScript
/**
|
|
* Tests for CleanupManager utility.
|
|
*
|
|
* Port: N/A (unit tests, no server)
|
|
*/
|
|
|
|
import { CleanupManager } from '../../src/utils/cleanup-manager.js';
|
|
|
|
describe('CleanupManager', () => {
|
|
describe('basic state', () => {
|
|
it('should start not disposed', () => {
|
|
const cleanup = new CleanupManager();
|
|
expect(cleanup.isDisposed).toBe(false);
|
|
expect(cleanup.isStopped).toBe(false);
|
|
expect(cleanup.resourceCount).toBe(0);
|
|
});
|
|
|
|
it('should track resource counts by type', () => {
|
|
const cleanup = new CleanupManager();
|
|
|
|
cleanup.setTimeout(() => {}, 10000);
|
|
cleanup.setInterval(() => {}, 10000);
|
|
cleanup.registerCleanup('watcher', () => {}, 'test watcher');
|
|
|
|
const counts = cleanup.resourceCounts;
|
|
expect(counts.timer).toBe(1);
|
|
expect(counts.interval).toBe(1);
|
|
expect(counts.watcher).toBe(1);
|
|
expect(counts.listener).toBe(0);
|
|
expect(counts.stream).toBe(0);
|
|
|
|
cleanup.dispose();
|
|
});
|
|
});
|
|
|
|
describe('setTimeout', () => {
|
|
it('should execute callback after delay', async () => {
|
|
const cleanup = new CleanupManager();
|
|
let called = false;
|
|
|
|
cleanup.setTimeout(() => { called = true; }, 50);
|
|
|
|
await new Promise(r => setTimeout(r, 100));
|
|
|
|
expect(called).toBe(true);
|
|
cleanup.dispose();
|
|
});
|
|
|
|
it('should not execute callback if disposed before delay', async () => {
|
|
const cleanup = new CleanupManager();
|
|
let called = false;
|
|
|
|
cleanup.setTimeout(() => { called = true; }, 100);
|
|
cleanup.dispose();
|
|
|
|
await new Promise(r => setTimeout(r, 150));
|
|
|
|
expect(called).toBe(false);
|
|
});
|
|
|
|
it('should remove registration when timer fires naturally', async () => {
|
|
const cleanup = new CleanupManager();
|
|
|
|
cleanup.setTimeout(() => {}, 50);
|
|
expect(cleanup.resourceCount).toBe(1);
|
|
|
|
await new Promise(r => setTimeout(r, 100));
|
|
|
|
expect(cleanup.resourceCount).toBe(0);
|
|
cleanup.dispose();
|
|
});
|
|
});
|
|
|
|
describe('setInterval', () => {
|
|
it('should execute callback repeatedly', async () => {
|
|
const cleanup = new CleanupManager();
|
|
let count = 0;
|
|
|
|
cleanup.setInterval(() => { count++; }, 30);
|
|
|
|
await new Promise(r => setTimeout(r, 100));
|
|
|
|
expect(count).toBeGreaterThanOrEqual(2);
|
|
cleanup.dispose();
|
|
});
|
|
|
|
it('should stop executing after dispose', async () => {
|
|
const cleanup = new CleanupManager();
|
|
let count = 0;
|
|
|
|
cleanup.setInterval(() => { count++; }, 20);
|
|
|
|
await new Promise(r => setTimeout(r, 50));
|
|
const countAtDispose = count;
|
|
cleanup.dispose();
|
|
|
|
await new Promise(r => setTimeout(r, 100));
|
|
|
|
expect(count).toBe(countAtDispose);
|
|
});
|
|
});
|
|
|
|
describe('registerCleanup', () => {
|
|
it('should call cleanup function on dispose', () => {
|
|
const cleanup = new CleanupManager();
|
|
let cleaned = false;
|
|
|
|
cleanup.registerCleanup('watcher', () => { cleaned = true; }, 'test');
|
|
cleanup.dispose();
|
|
|
|
expect(cleaned).toBe(true);
|
|
});
|
|
|
|
it('should continue cleanup even if one fails', () => {
|
|
const cleanup = new CleanupManager();
|
|
let secondCleaned = false;
|
|
|
|
cleanup.registerCleanup('watcher', () => { throw new Error('fail'); }, 'first');
|
|
cleanup.registerCleanup('watcher', () => { secondCleaned = true; }, 'second');
|
|
|
|
// Should not throw
|
|
cleanup.dispose();
|
|
|
|
expect(secondCleaned).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('registerWatcher', () => {
|
|
it('should call close() on dispose', () => {
|
|
const cleanup = new CleanupManager();
|
|
let closed = false;
|
|
const watcher = { close: () => { closed = true; } };
|
|
|
|
cleanup.registerWatcher(watcher, 'test watcher');
|
|
cleanup.dispose();
|
|
|
|
expect(closed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('registerListener', () => {
|
|
it('should call removeListener on dispose', () => {
|
|
const cleanup = new CleanupManager();
|
|
const listener = () => {};
|
|
let removed = false;
|
|
const emitter = {
|
|
removeListener: (event: string, fn: () => void) => {
|
|
if (event === 'data' && fn === listener) removed = true;
|
|
},
|
|
};
|
|
|
|
cleanup.registerListener(emitter, 'data', listener, 'test listener');
|
|
cleanup.dispose();
|
|
|
|
expect(removed).toBe(true);
|
|
});
|
|
|
|
it('should call off() if removeListener not available', () => {
|
|
const cleanup = new CleanupManager();
|
|
const listener = () => {};
|
|
let removed = false;
|
|
const emitter = {
|
|
off: (event: string, fn: () => void) => {
|
|
if (event === 'data' && fn === listener) removed = true;
|
|
},
|
|
};
|
|
|
|
cleanup.registerListener(emitter, 'data', listener, 'test listener');
|
|
cleanup.dispose();
|
|
|
|
expect(removed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('registerStream', () => {
|
|
it('should call destroy() on dispose', () => {
|
|
const cleanup = new CleanupManager();
|
|
let destroyed = false;
|
|
const stream = { destroy: () => { destroyed = true; } };
|
|
|
|
cleanup.registerStream(stream, 'test stream');
|
|
cleanup.dispose();
|
|
|
|
expect(destroyed).toBe(true);
|
|
});
|
|
|
|
it('should call close() if destroy not available', () => {
|
|
const cleanup = new CleanupManager();
|
|
let closed = false;
|
|
const stream = { close: () => { closed = true; } };
|
|
|
|
cleanup.registerStream(stream, 'test stream');
|
|
cleanup.dispose();
|
|
|
|
expect(closed).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('unregister', () => {
|
|
it('should manually remove and cleanup resource', () => {
|
|
const cleanup = new CleanupManager();
|
|
let cleaned = false;
|
|
|
|
const id = cleanup.registerCleanup('watcher', () => { cleaned = true; }, 'test');
|
|
expect(cleanup.resourceCount).toBe(1);
|
|
|
|
const result = cleanup.unregister(id);
|
|
|
|
expect(result).toBe(true);
|
|
expect(cleaned).toBe(true);
|
|
expect(cleanup.resourceCount).toBe(0);
|
|
|
|
cleanup.dispose();
|
|
});
|
|
|
|
it('should return false for unknown id', () => {
|
|
const cleanup = new CleanupManager();
|
|
|
|
const result = cleanup.unregister('unknown-id');
|
|
|
|
expect(result).toBe(false);
|
|
cleanup.dispose();
|
|
});
|
|
});
|
|
|
|
describe('dispose', () => {
|
|
it('should be idempotent (safe to call multiple times)', () => {
|
|
const cleanup = new CleanupManager();
|
|
let cleanupCount = 0;
|
|
|
|
cleanup.registerCleanup('watcher', () => { cleanupCount++; }, 'test');
|
|
|
|
cleanup.dispose();
|
|
cleanup.dispose();
|
|
cleanup.dispose();
|
|
|
|
expect(cleanupCount).toBe(1);
|
|
});
|
|
|
|
it('should mark as disposed', () => {
|
|
const cleanup = new CleanupManager();
|
|
|
|
cleanup.dispose();
|
|
|
|
expect(cleanup.isDisposed).toBe(true);
|
|
expect(cleanup.isStopped).toBe(true);
|
|
});
|
|
|
|
it('should clear all registrations', () => {
|
|
const cleanup = new CleanupManager();
|
|
|
|
cleanup.registerCleanup('watcher', () => {}, 'test1');
|
|
cleanup.registerCleanup('watcher', () => {}, 'test2');
|
|
expect(cleanup.resourceCount).toBe(2);
|
|
|
|
cleanup.dispose();
|
|
|
|
expect(cleanup.resourceCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('isStopped guard pattern', () => {
|
|
it('should allow checking isStopped in callbacks', async () => {
|
|
const cleanup = new CleanupManager();
|
|
let executedAfterStop = false;
|
|
|
|
cleanup.setTimeout(() => {
|
|
if (cleanup.isStopped) {
|
|
executedAfterStop = false;
|
|
} else {
|
|
executedAfterStop = true;
|
|
}
|
|
}, 50);
|
|
|
|
// Dispose before timer fires
|
|
cleanup.dispose();
|
|
|
|
await new Promise(r => setTimeout(r, 100));
|
|
|
|
// The callback shouldn't have set this to true
|
|
expect(executedAfterStop).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getRegistrations', () => {
|
|
it('should return all current registrations', () => {
|
|
const cleanup = new CleanupManager();
|
|
|
|
cleanup.setTimeout(() => {}, 10000, { description: 'timer1' });
|
|
cleanup.registerCleanup('watcher', () => {}, 'watcher1');
|
|
|
|
const regs = cleanup.getRegistrations();
|
|
|
|
expect(regs.length).toBe(2);
|
|
expect(regs.some(r => r.type === 'timer')).toBe(true);
|
|
expect(regs.some(r => r.type === 'watcher')).toBe(true);
|
|
|
|
cleanup.dispose();
|
|
});
|
|
});
|
|
});
|