/**
* Shopify Agentic MCP Gateway — Rate Limiter Middleware
*
* Token bucket algorithm for protecting Shopify API quota.
* Each key (e.g. shop domain, agent ID) gets its own bucket.
*/
interface TokenBucket {
tokens: number;
lastRefill: number;
}
export interface RateLimiterConfig {
/** Maximum tokens a bucket can hold */
maxTokens: number;
/** Number of tokens to add per refill */
refillRate: number;
/** Refill interval in milliseconds */
refillInterval: number;
}
export class RateLimiter {
private readonly maxTokens: number;
private readonly refillRate: number;
private readonly refillInterval: number;
private readonly buckets: Map<string, TokenBucket>;
constructor(config: RateLimiterConfig) {
this.maxTokens = config.maxTokens;
this.refillRate = config.refillRate;
this.refillInterval = config.refillInterval;
this.buckets = new Map();
}
/**
* Attempt to consume tokens from the bucket for the given key.
* Refills tokens based on elapsed time before checking availability.
*
* @param key - The rate limit key (e.g. shop domain, IP, agent ID)
* @param tokens - Number of tokens to consume (default: 1)
* @returns true if the tokens were consumed; false if rate limited
*/
tryConsume(key: string, tokens: number = 1): boolean {
const bucket = this.getOrCreateBucket(key);
this.refillBucket(bucket);
if (bucket.tokens >= tokens) {
bucket.tokens -= tokens;
return true;
}
return false;
}
/**
* Get the number of remaining tokens for a given key.
* Performs a refill calculation before returning.
*
* @param key - The rate limit key
* @returns Number of tokens currently available
*/
getRemainingTokens(key: string): number {
const bucket = this.buckets.get(key);
if (!bucket) {
return this.maxTokens;
}
this.refillBucket(bucket);
return Math.floor(bucket.tokens);
}
/**
* Reset a bucket to its maximum capacity.
*
* @param key - The rate limit key to reset
*/
reset(key: string): void {
this.buckets.delete(key);
}
/**
* Get or create a token bucket for the given key.
* New buckets start full.
*/
private getOrCreateBucket(key: string): TokenBucket {
let bucket = this.buckets.get(key);
if (!bucket) {
bucket = {
tokens: this.maxTokens,
lastRefill: Date.now(),
};
this.buckets.set(key, bucket);
}
return bucket;
}
/**
* Refill tokens in a bucket based on elapsed time since last refill.
* Tokens are capped at maxTokens.
*/
private refillBucket(bucket: TokenBucket): void {
const now = Date.now();
const elapsed = now - bucket.lastRefill;
if (elapsed <= 0) {
return;
}
const intervalsElapsed = elapsed / this.refillInterval;
const tokensToAdd = intervalsElapsed * this.refillRate;
if (tokensToAdd > 0) {
bucket.tokens = Math.min(this.maxTokens, bucket.tokens + tokensToAdd);
bucket.lastRefill = now;
}
}
}
/**
* Create a RateLimiter pre-configured with Shopify's standard API rate limits.
* Shopify allows 40 requests per second for REST Admin API (2 requests/second
* per bucket with a leak rate model, but bucket size of 40).
*
* @returns A RateLimiter configured for Shopify defaults
*/
export function createShopifyRateLimiter(): RateLimiter {
return new RateLimiter({
maxTokens: 40,
refillRate: 2,
refillInterval: 1000, // 1 second — 2 tokens restored per second
});
}