import type { Events, EventRef, EventRefImpl } from '../events' import { Debouncer } from './debouncer' /** * A subscription wrapper that debounces event delivery. * * When an event is triggered, the callback is not called immediately. * Instead, the latest arguments are stored and the callback is called * after the debounce delay. Rapid triggers coalesce into a single call. * * Implements EventRef interface for compatibility with Component.registerEvent(). * When Component.unload() calls offref(), pending debounced calls are cancelled. */ export class DebouncedEventRef implements EventRefImpl { /** Proxy Events instance that intercepts offref (for EventRef compatibility) */ readonly e: Events /** Event name (for EventRef compatibility) */ readonly n: string /** The internal callback registered with Events (for EventRef compatibility) */ readonly fn: (...data: unknown[]) => unknown private readonly debouncer: Debouncer private readonly actualEvents: Events private disposed = false constructor( events: Events, eventName: string, callback: (...data: unknown[]) => unknown, debounceMs: number ) { this.actualEvents = events this.n = eventName // Create debouncer that forwards to the user's callback this.debouncer = new Debouncer( (...args: unknown[]) => { if (!this.disposed) { callback(...args) } }, debounceMs ) // Create the inner callback that feeds the debouncer this.fn = (...args: unknown[]) => { if (!this.disposed) { this.debouncer.call(...args) } return undefined } // Subscribe to the actual events events.on(eventName, this.fn) // Create a proxy that intercepts offref to also dispose the debouncer. // When Component.unload() calls ref.e.offref(ref), our proxy intercepts // and calls dispose() to cancel pending debounced callbacks. // // Using JavaScript Proxy ensures all other Events methods are forwarded // automatically. This handles additions to the Events API, though signature // changes to offref would require updating this proxy. this.e = new Proxy(events, { get: (target: Events, prop: string | symbol): unknown => { if (prop === 'offref') { return (ref: EventRef): void => { if (ref === this) { this.dispose() } else { target.offref(ref) } } } // Forward all other property accesses to the real Events instance const value = target[prop as keyof typeof target] if (typeof value === 'function') { return value.bind(target) } return value }, }) as Events } /** * Flush any pending debounced call immediately. */ flush(): void { if (!this.disposed) { this.debouncer.flush() } } /** * Cancel any pending debounced call. * No-op if already disposed. */ cancel(): void { if (!this.disposed) { this.debouncer.cancel() } } /** * Unsubscribe and dispose the debouncer. * Pending calls are cancelled (not flushed). */ dispose(): void { if (this.disposed) return this.disposed = true this.actualEvents.off(this.n, this.fn) this.debouncer.dispose() } /** * Check if there is a pending debounced call. * Returns false if disposed. */ isPending(): boolean { if (this.disposed) return false return this.debouncer.isPending() } }