// engine/hooks/debouncer.ts — Debounce + rate limit for file events (SPEC-129)
// 300ms debounce per file, global rate limit per file per 5s,
// per-spec rate limit per 5 min. Token bucket algorithm.
// ---------------------------------------------------------------------------
// Token Bucket helpers
// ---------------------------------------------------------------------------
function createBucket(capacity, refillIntervalMs) {
    return {
        tokens: capacity,
        lastRefill: Date.now(),
        capacity,
        refillIntervalMs,
    };
}
function tryConsume(bucket, now) {
    // Refill tokens based on elapsed time
    const elapsed = now - bucket.lastRefill;
    const tokensToAdd = Math.floor(elapsed / bucket.refillIntervalMs);
    if (tokensToAdd > 0) {
        const updated = {
            ...bucket,
            tokens: Math.min(bucket.capacity, bucket.tokens + tokensToAdd),
            lastRefill: bucket.lastRefill + tokensToAdd * bucket.refillIntervalMs,
        };
        Object.assign(bucket, updated);
    }
    if (bucket.tokens > 0) {
        Object.assign(bucket, { tokens: bucket.tokens - 1 });
        return true;
    }
    return false;
}
// ---------------------------------------------------------------------------
// Debouncer
// ---------------------------------------------------------------------------
const DEFAULT_CONFIG = {
    debounceMs: 300,
    rateLimitPerFileMs: 5000,
    rateLimitPerSpecMs: 300_000,
};
export class Debouncer {
    config;
    /** Pending debounce timers per file path */
    pendingTimers = new Map();
    /** Pending event per file (latest wins) */
    pendingEvents = new Map();
    /** Rate limit buckets per file path */
    fileBuckets = new Map();
    /** Rate limit buckets per spec ID */
    specBuckets = new Map();
    callback;
    constructor(callback, config) {
        this.callback = callback;
        this.config = { ...DEFAULT_CONFIG, ...config };
    }
    /**
     * Feed a raw watcher event. The debouncer will coalesce rapid events
     * and apply rate limiting before invoking the callback.
     */
    push(event) {
        const key = event.path;
        // Store the latest event (coalescing — last wins)
        this.pendingEvents.set(key, event);
        // Clear existing timer for this path
        const existing = this.pendingTimers.get(key);
        if (existing !== undefined) {
            clearTimeout(existing);
        }
        // Set new debounce timer
        const timer = setTimeout(() => {
            this.pendingTimers.delete(key);
            const latest = this.pendingEvents.get(key);
            this.pendingEvents.delete(key);
            /* v8 ignore start -- latest is always defined since timer holds the key */
            if (latest) {
                this.tryEmit(latest);
            }
            /* v8 ignore stop */
        }, this.config.debounceMs);
        this.pendingTimers.set(key, timer);
    }
    /**
     * Check spec-level rate limit for auto-reconcile.
     * Returns true if allowed, false if rate-limited.
     */
    checkSpecRateLimit(specId) {
        const now = Date.now();
        let bucket = this.specBuckets.get(specId);
        if (!bucket) {
            bucket = createBucket(1, this.config.rateLimitPerSpecMs);
            this.specBuckets.set(specId, bucket);
        }
        return tryConsume(bucket, now);
    }
    /**
     * Cancel all pending timers and clear state.
     */
    dispose() {
        for (const timer of this.pendingTimers.values()) {
            clearTimeout(timer);
        }
        this.pendingTimers.clear();
        this.pendingEvents.clear();
        this.fileBuckets.clear();
        this.specBuckets.clear();
    }
    // ---------------------------------------------------------------------------
    // Private
    // ---------------------------------------------------------------------------
    tryEmit(event) {
        const now = Date.now();
        const key = event.path;
        // File-level rate limit
        let bucket = this.fileBuckets.get(key);
        if (!bucket) {
            bucket = createBucket(1, this.config.rateLimitPerFileMs);
            this.fileBuckets.set(key, bucket);
        }
        if (!tryConsume(bucket, now)) {
            // Rate limited — drop the event
            return;
        }
        this.callback(event);
    }
}
//# sourceMappingURL=debouncer.js.map