// in-memory leaky-bucket throttle for `POST /spawn role=public`.
//
// Two buckets fire on each admit attempt: one keyed `(senderId, channel)` to
// stop a single sender from flooding one channel, and one keyed `senderId`
// alone to stop the same sender from working around the per-channel cap by
// rotating channels. First bucket to trip wins; `scope` in the result names
// which one so the 429 body and log line can tell them apart.
//
// Out of scope (per task): persistence across manager restarts, cross-process
// coordination. Restarts wipe the buckets; the manager is single-process per
// brand on the Pi.
function evict(stamps, cutoff) {
    // Stamps are pushed in monotonic order; drop the prefix older than cutoff.
    let i = 0;
    while (i < stamps.length && stamps[i] <= cutoff)
        i++;
    return i === 0 ? stamps : stamps.slice(i);
}
export function createSpawnRateLimiter(limits) {
    const state = {
        byChannel: new Map(),
        byGlobal: new Map(),
    };
    return {
        admit(senderId, channel, now = Date.now()) {
            const channelLimit = limits.perChannel[channel];
            const channelKey = `${senderId}|${channel}`;
            const channelCutoff = now - channelLimit.windowMs;
            const channelStamps = evict(state.byChannel.get(channelKey) ?? [], channelCutoff);
            if (channelStamps.length >= channelLimit.cap) {
                // Persist the eviction so the map doesn't accumulate stale entries.
                state.byChannel.set(channelKey, channelStamps);
                return {
                    admitted: false,
                    scope: 'channel',
                    cap: channelLimit.cap,
                    windowMs: channelLimit.windowMs,
                };
            }
            const globalCutoff = now - limits.global.windowMs;
            const globalStamps = evict(state.byGlobal.get(senderId) ?? [], globalCutoff);
            if (globalStamps.length >= limits.global.cap) {
                state.byChannel.set(channelKey, channelStamps);
                state.byGlobal.set(senderId, globalStamps);
                return {
                    admitted: false,
                    scope: 'global',
                    cap: limits.global.cap,
                    windowMs: limits.global.windowMs,
                };
            }
            channelStamps.push(now);
            globalStamps.push(now);
            state.byChannel.set(channelKey, channelStamps);
            state.byGlobal.set(senderId, globalStamps);
            return { admitted: true };
        },
        reset() {
            state.byChannel.clear();
            state.byGlobal.clear();
        },
    };
}
export const DEFAULT_PUBLIC_SPAWN_LIMITS = {
    // Conservative defaults: 10 spawns / minute per channel; 30 / minute global.
    // Operators override via brand.json or env. verification fires the
    // per-channel cap (11 calls, same senderId+channel).
    perChannel: {
        browser: { cap: 10, windowMs: 60_000 },
        whatsapp: { cap: 10, windowMs: 60_000 },
        telegram: { cap: 10, windowMs: 60_000 },
        webchat: { cap: 10, windowMs: 60_000 },
        email: { cap: 10, windowMs: 60_000 },
    },
    global: { cap: 30, windowMs: 60_000 },
};
//# sourceMappingURL=spawn-rate-limiter.js.map