/**
 * Backpressure protections for the event router, wrapped around an inner
 * dispatcher. Two protections, both default-on:
 *
 *   1. Per-subscription concurrency cap = 1. If an event arrives while a
 *      previous run for the same subscription is in flight, drop the new
 *      one. Stops a `git pull` (1000 file-change events in 200ms) from
 *      blowing up into 1000 parallel subagent runs.
 *
 *   2. Trailing-edge debounce, 500ms, applied to `file.changed` events
 *      only. Events within the window reset the timer; on expiry, the most
 *      recent event is dispatched once. Cron and future kinds opt in as
 *      they prove noisy.
 *
 * Per the plan, neither protection has v1 config knobs (debounceMs is
 * available here only as a testing seam).
 *
 * See `agents/2026-05-20-skills-unification-plan.md` step 11.
 */
const DEFAULT_DEBOUNCE_MS = 500;
export class BackpressureDispatcher {
    inner;
    options;
    inFlight = new Set();
    debouncers = new Map();
    pendingEvents = new Map();
    constructor(inner, options = {}) {
        this.inner = inner;
        this.options = options;
    }
    dispatch(sub, event) {
        if (event.kind === 'file.changed') {
            this.scheduleDebounced(sub, event);
            return;
        }
        return this.dispatchImmediate(sub, event);
    }
    scheduleDebounced(sub, event) {
        this.pendingEvents.set(sub.id, { sub, event });
        const existing = this.debouncers.get(sub.id);
        if (existing)
            clearTimeout(existing);
        const timer = setTimeout(() => {
            this.debouncers.delete(sub.id);
            const pending = this.pendingEvents.get(sub.id);
            if (pending) {
                this.pendingEvents.delete(sub.id);
                void this.dispatchImmediate(pending.sub, pending.event);
            }
        }, this.options.debounceMs ?? DEFAULT_DEBOUNCE_MS);
        this.debouncers.set(sub.id, timer);
    }
    async dispatchImmediate(sub, event) {
        if (this.inFlight.has(sub.id)) {
            this.options.onDrop?.(sub, event);
            return;
        }
        this.inFlight.add(sub.id);
        try {
            await this.inner.dispatch(sub, event);
        }
        finally {
            this.inFlight.delete(sub.id);
        }
    }
    /**
     * Clear pending debouncers. The daemon calls this on shutdown so timers
     * don't keep the event loop alive. In-flight runs are not interrupted
     * (they're the inner dispatcher's responsibility).
     */
    dispose() {
        for (const timer of this.debouncers.values())
            clearTimeout(timer);
        this.debouncers.clear();
        this.pendingEvents.clear();
    }
}
//# sourceMappingURL=backpressure.js.map