/** * Event Throttler - Optimizes high-frequency event emissions * * Provides throttling and batching for ticker events to reduce overhead * and improve UI rendering performance */ export interface EventThrottlerConfig { /** Throttle interval in milliseconds (0 = disabled) */ throttleInterval?: number; /** Enable batching mode */ enableBatching?: boolean; /** Maximum batch size before auto-flush */ maxBatchSize?: number; } /** * EventThrottler class for optimizing event emissions */ export class EventThrottler { private readonly config: Required; private throttleTimer: ReturnType | null = null; private rafId: number | null = null; private lastEvent: T | null = null; private eventBatch: T[] = []; private readonly emitFn: (events: T | T[]) => void; constructor(emitFn: (events: T | T[]) => void, config: EventThrottlerConfig = {}) { this.emitFn = emitFn; this.config = { throttleInterval: config.throttleInterval ?? 16, // 60 FPS enableBatching: config.enableBatching ?? false, maxBatchSize: config.maxBatchSize ?? 10, }; } /** * Emit event (with throttling/batching) */ public emit(event: T): void { // No throttling - emit immediately if (this.config.throttleInterval === 0) { this.emitFn(event); return; } // Batching mode if (this.config.enableBatching) { this.addToBatch(event); return; } // Throttling mode (last event wins) this.lastEvent = event; this.scheduleEmit(); } /** * Flush pending events immediately */ public flush(): void { this.cancelScheduled(); if (this.config.enableBatching) { this.flushBatch(); } else if (this.lastEvent !== null) { this.emitFn(this.lastEvent); this.lastEvent = null; } } /** * Destroy throttler and cleanup resources */ public destroy(): void { this.flush(); this.cancelScheduled(); this.eventBatch = []; this.lastEvent = null; } /** * Add event to batch */ private addToBatch(event: T): void { this.eventBatch.push(event); // Auto-flush if batch size reached if (this.eventBatch.length >= this.config.maxBatchSize) { this.flushBatch(); } else { this.scheduleEmit(); } } /** * Flush batch */ private flushBatch(): void { if (this.eventBatch.length === 0) { return; } const batch = this.eventBatch.splice(0, this.eventBatch.length); this.emitFn(batch); } /** * Schedule emit */ private scheduleEmit(): void { if (this.throttleTimer !== null || this.rafId !== null) { return; // Already scheduled } // Use requestAnimationFrame for browser (smoother) if (typeof requestAnimationFrame !== 'undefined') { this.rafId = requestAnimationFrame(() => { this.rafId = null; // Use setTimeout for throttle interval this.throttleTimer = setTimeout(() => { this.throttleTimer = null; if (this.config.enableBatching) { this.flushBatch(); } else if (this.lastEvent !== null) { this.emitFn(this.lastEvent); this.lastEvent = null; } }, this.config.throttleInterval); }); } else { // Fallback to setTimeout for Node.js this.throttleTimer = setTimeout(() => { this.throttleTimer = null; if (this.config.enableBatching) { this.flushBatch(); } else if (this.lastEvent !== null) { this.emitFn(this.lastEvent); this.lastEvent = null; } }, this.config.throttleInterval); } } /** * Cancel scheduled emit */ private cancelScheduled(): void { if (this.throttleTimer !== null) { clearTimeout(this.throttleTimer); this.throttleTimer = null; } if (this.rafId !== null && typeof cancelAnimationFrame !== 'undefined') { cancelAnimationFrame(this.rafId); this.rafId = null; } } }