import SyncError from "../../errors/SyncError.js";
const MILLISECONDS_PER_SECOND = 1000;
function validateCapacity(capacity) {
    if (!Number.isInteger(capacity) || capacity <= 0) {
        throw new SyncError('rateLimit.capacity must be a positive integer', {
            code: 'E_SYNC_RATE_LIMIT_CONFIG',
            context: { capacity },
        });
    }
}
function validateRefillRate(refillTokensPerSecond) {
    if (!Number.isFinite(refillTokensPerSecond) || refillTokensPerSecond <= 0) {
        throw new SyncError('rateLimit.refillTokensPerSecond must be a positive number', {
            code: 'E_SYNC_RATE_LIMIT_CONFIG',
            context: { refillTokensPerSecond },
        });
    }
}
function validateClock(clock) {
    if (typeof clock !== 'function') {
        throw new SyncError('rateLimit.clock must be a function', {
            code: 'E_SYNC_RATE_LIMIT_CONFIG',
            context: {},
        });
    }
}
export default class SyncRateLimiter {
    _capacity;
    _refillTokensPerSecond;
    _clock;
    _buckets;
    constructor(config) {
        validateCapacity(config.capacity);
        validateRefillRate(config.refillTokensPerSecond);
        validateClock(config.clock);
        this._capacity = config.capacity;
        this._refillTokensPerSecond = config.refillTokensPerSecond;
        this._clock = config.clock;
        this._buckets = new Map();
        Object.freeze(this);
    }
    tryConsume(keyId) {
        const nowMs = this._readClock();
        const current = this._buckets.get(keyId) ?? {
            tokens: this._capacity,
            updatedAtMs: nowMs,
        };
        const refreshed = this._refreshedBucket(current, nowMs);
        if (refreshed.tokens < 1) {
            this._buckets.set(keyId, refreshed);
            return false;
        }
        this._buckets.set(keyId, {
            tokens: refreshed.tokens - 1,
            updatedAtMs: refreshed.updatedAtMs,
        });
        return true;
    }
    _readClock() {
        const nowMs = this._clock();
        if (!Number.isFinite(nowMs)) {
            throw new SyncError('rateLimit.clock must return a finite millisecond value', {
                code: 'E_SYNC_RATE_LIMIT_CLOCK',
                context: { nowMs },
            });
        }
        return nowMs;
    }
    _refreshedBucket(state, nowMs) {
        const elapsedMs = nowMs - state.updatedAtMs;
        if (elapsedMs <= 0) {
            return {
                tokens: Math.min(this._capacity, state.tokens),
                updatedAtMs: state.updatedAtMs,
            };
        }
        const refill = (elapsedMs / MILLISECONDS_PER_SECOND) * this._refillTokensPerSecond;
        return {
            tokens: Math.min(this._capacity, state.tokens + refill),
            updatedAtMs: nowMs,
        };
    }
}
