import { RateLimitError } from "./errors.js";
export class RateLimiter {
    memoryStorage;
    points;
    duration;
    blockDuration;
    keyPrefix;
    constructor(opts = {}) {
        this.points = opts.points ?? 4;
        this.duration = opts.duration ?? 1;
        this.blockDuration = opts.blockDuration ?? 0;
        this.keyPrefix = opts.keyPrefix ?? 'rlflx';
        this.memoryStorage = new MemoryStorage();
    }
    consume(key, pointsToConsume = 1, options = {}) {
        const rlKey = this.getKey(key);
        const secDuration = this._getKeySecDuration(options);
        let res = this.memoryStorage.incrby(rlKey, pointsToConsume, secDuration);
        res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
        if (res.consumedPoints > this.points) {
            // Block only first time when consumed more than points
            if (this.blockDuration > 0 && res.consumedPoints <= (this.points + pointsToConsume)) {
                // Block key
                res = this.memoryStorage.set(rlKey, res.consumedPoints, this.blockDuration);
            }
            throw new RateLimitError('Rate limit exceeded', res);
        }
        return res;
    }
    penalty(key, points = 1, options = {}) {
        const rlKey = this.getKey(key);
        const secDuration = this._getKeySecDuration(options);
        const res = this.memoryStorage.incrby(rlKey, points, secDuration);
        res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
        return res;
    }
    reward(key, points = 1, options = {}) {
        const rlKey = this.getKey(key);
        const secDuration = this._getKeySecDuration(options);
        const res = this.memoryStorage.incrby(rlKey, -points, secDuration);
        res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
        return res;
    }
    /**
     * Block any key for secDuration seconds
     *
     * @param key
     * @param secDuration
     */
    block(key, secDuration) {
        const msDuration = secDuration * 1000;
        const initPoints = this.points + 1;
        this.memoryStorage.set(this.getKey(key), initPoints, secDuration);
        return {
            remainingPoints: 0,
            msBeforeNext: msDuration === 0 ? -1 : msDuration,
            consumedPoints: initPoints,
            isFirstInDuration: false
        };
    }
    set(key, points, secDuration = 0) {
        const msDuration = (secDuration >= 0 ? secDuration : this.duration) * 1000;
        this.memoryStorage.set(this.getKey(key), points, secDuration);
        return {
            remainingPoints: 0,
            msBeforeNext: msDuration === 0 ? -1 : msDuration,
            consumedPoints: points,
            isFirstInDuration: false
        };
    }
    get(key) {
        const res = this.memoryStorage.get(this.getKey(key));
        if (res != null) {
            res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
        }
        return res;
    }
    delete(key) {
        this.memoryStorage.delete(this.getKey(key));
    }
    _getKeySecDuration(options) {
        if (options?.customDuration != null && options.customDuration >= 0) {
            return options.customDuration;
        }
        return this.duration;
    }
    getKey(key) {
        return this.keyPrefix.length > 0 ? `${this.keyPrefix}:${key}` : key;
    }
    parseKey(rlKey) {
        return rlKey.substring(this.keyPrefix.length);
    }
}
export class MemoryStorage {
    storage;
    constructor() {
        this.storage = new Map();
    }
    incrby(key, value, durationSec) {
        const existing = this.storage.get(key);
        if (existing != null) {
            const msBeforeExpires = existing.expiresAt != null
                ? existing.expiresAt.getTime() - new Date().getTime()
                : -1;
            if (existing.expiresAt == null || msBeforeExpires > 0) {
                // Change value
                existing.value += value;
                return {
                    remainingPoints: 0,
                    msBeforeNext: msBeforeExpires,
                    consumedPoints: existing.value,
                    isFirstInDuration: false
                };
            }
            return this.set(key, value, durationSec);
        }
        return this.set(key, value, durationSec);
    }
    set(key, value, durationSec) {
        const durationMs = durationSec * 1000;
        const existing = this.storage.get(key);
        if (existing != null) {
            clearTimeout(existing.timeoutId);
        }
        const record = {
            value,
            expiresAt: durationMs > 0 ? new Date(Date.now() + durationMs) : undefined
        };
        this.storage.set(key, record);
        if (durationMs > 0) {
            record.timeoutId = setTimeout(() => {
                this.storage.delete(key);
            }, durationMs);
            if (record.timeoutId.unref != null) {
                record.timeoutId.unref();
            }
        }
        return {
            remainingPoints: 0,
            msBeforeNext: durationMs === 0 ? -1 : durationMs,
            consumedPoints: record.value,
            isFirstInDuration: true
        };
    }
    get(key) {
        const existing = this.storage.get(key);
        if (existing != null) {
            const msBeforeExpires = existing.expiresAt != null
                ? existing.expiresAt.getTime() - new Date().getTime()
                : -1;
            return {
                remainingPoints: 0,
                msBeforeNext: msBeforeExpires,
                consumedPoints: existing.value,
                isFirstInDuration: false
            };
        }
    }
    delete(key) {
        const record = this.storage.get(key);
        if (record != null) {
            if (record.timeoutId != null) {
                clearTimeout(record.timeoutId);
            }
            this.storage.delete(key);
            return true;
        }
        return false;
    }
}
//# sourceMappingURL=rate-limiter.js.map