@async-kit/ratelimitx 0.1.25 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.2.0 (2026-03-12)
2
+
3
+ ### 🚀 Features
4
+
5
+ - add cachex, eventx, and workflowx packages ([50277b9](https://github.com/NexaLeaf/async-kit/commit/50277b9))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Palanisamy Muthusamy
10
+
11
+
12
+
1
13
 
2
14
 
3
15
 
package/README.md CHANGED
@@ -104,10 +104,12 @@ const fw = new FixedWindow({
104
104
  | `.tryAcquire()` | Non-blocking; `true` if under limit |
105
105
  | `.acquire()` | Throws `RateLimitError` if at limit |
106
106
  | `.waitAndAcquire(signal?)` | Async — waits until the window resets |
107
- | `.reset()` | Manually reset count (useful in tests) |
107
+ | `.reset()` | Manually reset count + fires `onWindowReset` callback |
108
108
  | `.currentCount` | Requests in current window |
109
109
  | `.windowResetMs` | Ms remaining until the window resets |
110
110
 
111
+ > **`reset()` fires `onWindowReset`** — the callback is called on both automatic resets and manual `.reset()` calls, keeping monitoring/UI state consistent.
112
+
111
113
  ### `CompositeLimiter`
112
114
 
113
115
  Enforces **all** provided limiters simultaneously. Useful for multi-tier API limits.
@@ -124,7 +126,9 @@ const limiter = new CompositeLimiter([
124
126
  |---|---|
125
127
  | `.tryAcquire()` | `true` only if **all** limiters pass |
126
128
  | `.acquire()` | Throws on the first limiter that rejects |
127
- | `.waitAndAcquire(signal?)` | Async — waits until all limiters have capacity |
129
+ | `.waitAndAcquire(signal?)` | Async — sleeps for the actual `retryAfterMs` of the blocking limiter (no busy-poll) |
130
+
131
+ > **Smart wait:** `waitAndAcquire` reads `RateLimitError.retryAfterMs` from whichever limiter blocks and sleeps exactly that long — no 1 ms polling loop.
128
132
 
129
133
  ### `RateLimitError`
130
134
 
package/dist/index.js CHANGED
@@ -244,6 +244,7 @@ var FixedWindow = class {
244
244
  reset() {
245
245
  this.count = 0;
246
246
  this.windowStart = Date.now();
247
+ this.onWindowReset?.(this.windowStart);
247
248
  }
248
249
  };
249
250
  var CompositeLimiter = class {
@@ -268,9 +269,27 @@ var CompositeLimiter = class {
268
269
  }
269
270
  /** Waits for ALL limiters to have capacity, then acquires atomically. */
270
271
  async waitAndAcquire(signal) {
271
- while (!this.tryAcquire()) {
272
+ while (true) {
272
273
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
273
- await abortableDelay(1, signal);
274
+ let waitMs = 0;
275
+ let allPassed = true;
276
+ for (const l of this.limiters) {
277
+ if (l.tryAcquire()) ; else {
278
+ allPassed = false;
279
+ try {
280
+ l.acquire();
281
+ } catch (err) {
282
+ if (err instanceof RateLimitError) {
283
+ waitMs = Math.max(waitMs, err.retryAfterMs || 1);
284
+ } else {
285
+ waitMs = Math.max(waitMs, 1);
286
+ }
287
+ }
288
+ break;
289
+ }
290
+ }
291
+ if (allPassed) return;
292
+ await abortableDelay(Math.max(1, waitMs), signal);
274
293
  }
275
294
  }
276
295
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ratelimitx.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type { TokenBucketOptions, SlidingWindowOptions, FixedWindowOptions, Limiter, RateLimitAlgorithm } from './types.js';\nimport type { TokenBucketOptions, SlidingWindowOptions, FixedWindowOptions, Limiter } from './types.js';\n\nfunction abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) { reject(new DOMException('Aborted', 'AbortError')); return; }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); }, { once: true });\n });\n}\n\n// ─── Error ───────────────────────────────────────────────────────────────────\n\nexport class RateLimitError extends Error {\n constructor(\n message: string,\n public readonly retryAfterMs: number,\n public readonly algorithm: 'token-bucket' | 'sliding-window' | 'fixed-window',\n public readonly limit: number,\n public readonly current: number\n ) {\n super(message);\n this.name = 'RateLimitError';\n }\n}\n\n// ─── Shared Helpers ──────────────────────────────────────────────────────────\n\n/**\n * Token Bucket rate limiter.\n *\n * Tokens accumulate over time up to `capacity`. Burst-friendly — allows up\n * to `capacity` back-to-back calls as long as the bucket is full.\n *\n * - `tryConsume()` — non-throwing, returns `true` if tokens available.\n * - `consume()` — async, **blocks** until tokens are available.\n * - `available` — current token count (after refill).\n *\n * @example\n * const bucket = new TokenBucket({ capacity: 10, refillRate: 2, refillInterval: 1000 });\n * await bucket.consume(); // waits until a token is available\n */\nexport class TokenBucket {\n private tokens: number;\n private lastRefillTime: number;\n private capacity: number;\n private readonly refillRate: number;\n private readonly refillInterval: number;\n\n constructor(options: TokenBucketOptions) {\n this.capacity = options.capacity;\n this.refillRate = options.refillRate;\n this.refillInterval = options.refillInterval ?? 1000;\n this.tokens = options.capacity;\n this.lastRefillTime = Date.now();\n }\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefillTime;\n if (elapsed <= 0) return;\n const newTokens = (elapsed / this.refillInterval) * this.refillRate;\n if (newTokens >= 1) {\n // Advance lastRefillTime only by the consumed portion to preserve sub-interval leftovers\n const consumed = Math.floor(newTokens);\n this.tokens = Math.min(this.capacity, this.tokens + consumed);\n this.lastRefillTime += Math.floor(consumed * (this.refillInterval / this.refillRate));\n }\n }\n\n get available(): number {\n this.refill();\n return this.tokens;\n }\n\n tryConsume(count = 1): boolean {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return true;\n }\n return false;\n }\n\n /**\n * Waits until `count` tokens are available, then consumes them.\n * Throws `RateLimitError` only if the request can never be satisfied\n * (i.e. `count > capacity`). Cancellable via `AbortSignal`.\n */\n async consume(count = 1, signal?: AbortSignal): Promise<void> {\n if (count > this.capacity) {\n throw new RateLimitError(\n `Requested ${count} tokens exceeds capacity ${this.capacity}`,\n Infinity,\n 'token-bucket',\n this.capacity,\n count\n );\n }\n while (true) {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n const deficit = count - this.tokens;\n const waitMs = Math.ceil((deficit / this.refillRate) * this.refillInterval);\n await abortableDelay(waitMs, signal);\n }\n }\n\n /** Alias for `acquireOrThrow()` — satisfies the `Limiter` interface. */\n acquire(count = 1): void {\n this.acquireOrThrow(count);\n }\n\n /** Satisfies the `Limiter` interface — alias for `tryConsume()`. */\n tryAcquire(): boolean {\n return this.tryConsume();\n }\n\n /** Satisfies the `Limiter` interface — alias for `consume()`. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n return this.consume(1, signal);\n }\n\n /** Throws immediately if tokens are unavailable (non-blocking equivalent of `consume`). */\n acquireOrThrow(count = 1): void {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n const deficit = count - this.tokens;\n const retryAfterMs = Math.ceil((deficit / this.refillRate) * this.refillInterval);\n throw new RateLimitError(\n `Rate limit exceeded. Retry after ${retryAfterMs}ms`,\n retryAfterMs,\n 'token-bucket',\n this.capacity,\n Math.floor(this.tokens)\n );\n }\n\n /** Refill to full capacity and reset the refill clock. */\n reset(): void {\n this.tokens = this.capacity;\n this.lastRefillTime = Date.now();\n }\n\n /** Hot-resize capacity. Clamps current tokens if the new capacity is lower. */\n setCapacity(capacity: number): void {\n if (capacity < 1) throw new RangeError('capacity must be >= 1');\n this.capacity = capacity as typeof this.capacity;\n this.tokens = Math.min(this.tokens, capacity);\n }\n}\n\n// ─── Sliding Window ───────────────────────────────────────────────────────────\n\n/**\n * Sliding Window rate limiter.\n *\n * Tracks exact request timestamps in a ring buffer. Strict enforcement —\n * no burst beyond `maxRequests` regardless of spacing.\n *\n * - `tryAcquire()` — non-throwing.\n * - `acquire()` — throws `RateLimitError` immediately.\n * - `waitAndAcquire()` — async, blocks until a slot is available.\n *\n * @example\n * const window = new SlidingWindow({ windowMs: 60_000, maxRequests: 100 });\n * await window.waitAndAcquire(); // queues caller until a slot opens\n */\nexport class SlidingWindow {\n // Ring buffer: pre-allocated array + head index avoids O(n) Array.shift()\n private readonly ring: Float64Array;\n private head = 0;\n private count = 0;\n private readonly windowMs: number;\n private readonly maxRequests: number;\n\n constructor(options: SlidingWindowOptions) {\n this.windowMs = options.windowMs;\n this.maxRequests = options.maxRequests;\n this.ring = new Float64Array(options.maxRequests);\n }\n\n private prune(): void {\n const cutoff = Date.now() - this.windowMs;\n while (this.count > 0 && this.ring[this.head % this.maxRequests] < cutoff) {\n this.head++;\n this.count--;\n }\n }\n\n get currentCount(): number {\n this.prune();\n return this.count;\n }\n\n tryAcquire(): boolean {\n this.prune();\n if (this.count < this.maxRequests) {\n this.ring[(this.head + this.count) % this.maxRequests] = Date.now();\n this.count++;\n return true;\n }\n return false;\n }\n\n acquire(): void {\n this.prune();\n if (this.count < this.maxRequests) {\n this.ring[(this.head + this.count) % this.maxRequests] = Date.now();\n this.count++;\n return;\n }\n const oldest = this.ring[this.head % this.maxRequests];\n const retryAfterMs = Math.max(0, oldest + this.windowMs - Date.now());\n throw new RateLimitError(\n `Rate limit exceeded. ${this.maxRequests} requests per ${this.windowMs}ms window.`,\n retryAfterMs,\n 'sliding-window',\n this.maxRequests,\n this.count\n );\n }\n\n /** Blocks until a slot is available, then acquires it. Cancellable via AbortSignal. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n const oldest = this.ring[this.head % this.maxRequests];\n const waitMs = Math.max(1, oldest + this.windowMs - Date.now());\n await abortableDelay(waitMs, signal);\n }\n }\n}\n\n// ─── Fixed Window ─────────────────────────────────────────────────────────────\n\n/**\n * Fixed Window rate limiter.\n *\n * Simplest algorithm — counts requests in a fixed time bucket that resets\n * every `windowMs`. Easiest to reason about; susceptible to boundary spikes.\n *\n * @example\n * const fw = new FixedWindow({ windowMs: 60_000, maxRequests: 100 });\n * fw.acquire(); // throws if over limit in current window\n */\nexport class FixedWindow {\n private count = 0;\n private windowStart: number;\n private readonly windowMs: number;\n private readonly maxRequests: number;\n private readonly onWindowReset?: (windowStart: number) => void;\n\n constructor(options: FixedWindowOptions) {\n this.windowMs = options.windowMs;\n this.maxRequests = options.maxRequests;\n this.onWindowReset = options.onWindowReset;\n this.windowStart = Date.now();\n }\n\n private tick(): void {\n const now = Date.now();\n if (now - this.windowStart >= this.windowMs) {\n this.count = 0;\n this.windowStart = now;\n this.onWindowReset?.(this.windowStart);\n }\n }\n\n get currentCount(): number {\n this.tick();\n return this.count;\n }\n\n /** Ms until the current window resets. */\n get windowResetMs(): number {\n return Math.max(0, this.windowStart + this.windowMs - Date.now());\n }\n\n tryAcquire(): boolean {\n this.tick();\n if (this.count < this.maxRequests) {\n this.count++;\n return true;\n }\n return false;\n }\n\n acquire(): void {\n this.tick();\n if (this.count < this.maxRequests) {\n this.count++;\n return;\n }\n throw new RateLimitError(\n `Fixed window rate limit exceeded. Retry after ${this.windowResetMs}ms`,\n this.windowResetMs,\n 'fixed-window',\n this.maxRequests,\n this.count\n );\n }\n\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n await abortableDelay(this.windowResetMs || 1, signal);\n }\n }\n\n /** Manually reset the window (useful in tests). */\n reset(): void {\n this.count = 0;\n this.windowStart = Date.now();\n }\n}\n\n// ─── Composite Limiter ────────────────────────────────────────────────────────\n\n/**\n * Composite rate limiter — enforces multiple limits simultaneously.\n *\n * All limiters must pass for a call to be allowed. Useful when APIs enforce\n * multiple tiers (e.g. 10/second AND 500/minute AND 5000/hour).\n *\n * @example\n * const limiter = new CompositeLimiter([\n * new TokenBucket({ capacity: 10, refillRate: 10, refillInterval: 1000 }),\n * new SlidingWindow({ windowMs: 60_000, maxRequests: 500 }),\n * ]);\n * await limiter.waitAndAcquire();\n */\nexport class CompositeLimiter {\n constructor(private readonly limiters: Limiter[]) {\n if (limiters.length === 0) throw new RangeError('CompositeLimiter requires at least one limiter');\n }\n\n /** Returns `true` only if ALL limiters can acquire. Rolls back on partial failure. */\n tryAcquire(): boolean {\n const acquired: Limiter[] = [];\n for (const l of this.limiters) {\n if (l.tryAcquire()) {\n acquired.push(l);\n } else {\n // Rollback is not possible for SlidingWindow/FixedWindow after tryAcquire succeeds,\n // but we stop here — already-acquired counts against the window, which is acceptable.\n return false;\n }\n }\n return true;\n }\n\n /** Throws `RateLimitError` with the most restrictive `retryAfterMs`. */\n acquire(): void {\n for (const l of this.limiters) {\n l.acquire();\n }\n }\n\n /** Waits for ALL limiters to have capacity, then acquires atomically. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');\n await abortableDelay(1, signal);\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/ratelimitx.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type { TokenBucketOptions, SlidingWindowOptions, FixedWindowOptions, Limiter, RateLimitAlgorithm } from './types.js';\nimport type { TokenBucketOptions, SlidingWindowOptions, FixedWindowOptions, Limiter } from './types.js';\n\nfunction abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) { reject(new DOMException('Aborted', 'AbortError')); return; }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); }, { once: true });\n });\n}\n\n// ─── Error ───────────────────────────────────────────────────────────────────\n\nexport class RateLimitError extends Error {\n constructor(\n message: string,\n public readonly retryAfterMs: number,\n public readonly algorithm: 'token-bucket' | 'sliding-window' | 'fixed-window',\n public readonly limit: number,\n public readonly current: number\n ) {\n super(message);\n this.name = 'RateLimitError';\n }\n}\n\n// ─── Shared Helpers ──────────────────────────────────────────────────────────\n\n/**\n * Token Bucket rate limiter.\n *\n * Tokens accumulate over time up to `capacity`. Burst-friendly — allows up\n * to `capacity` back-to-back calls as long as the bucket is full.\n *\n * - `tryConsume()` — non-throwing, returns `true` if tokens available.\n * - `consume()` — async, **blocks** until tokens are available.\n * - `available` — current token count (after refill).\n *\n * @example\n * const bucket = new TokenBucket({ capacity: 10, refillRate: 2, refillInterval: 1000 });\n * await bucket.consume(); // waits until a token is available\n */\nexport class TokenBucket {\n private tokens: number;\n private lastRefillTime: number;\n private capacity: number;\n private readonly refillRate: number;\n private readonly refillInterval: number;\n\n constructor(options: TokenBucketOptions) {\n this.capacity = options.capacity;\n this.refillRate = options.refillRate;\n this.refillInterval = options.refillInterval ?? 1000;\n this.tokens = options.capacity;\n this.lastRefillTime = Date.now();\n }\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefillTime;\n if (elapsed <= 0) return;\n const newTokens = (elapsed / this.refillInterval) * this.refillRate;\n if (newTokens >= 1) {\n // Advance lastRefillTime only by the consumed portion to preserve sub-interval leftovers\n const consumed = Math.floor(newTokens);\n this.tokens = Math.min(this.capacity, this.tokens + consumed);\n this.lastRefillTime += Math.floor(consumed * (this.refillInterval / this.refillRate));\n }\n }\n\n get available(): number {\n this.refill();\n return this.tokens;\n }\n\n tryConsume(count = 1): boolean {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return true;\n }\n return false;\n }\n\n /**\n * Waits until `count` tokens are available, then consumes them.\n * Throws `RateLimitError` only if the request can never be satisfied\n * (i.e. `count > capacity`). Cancellable via `AbortSignal`.\n */\n async consume(count = 1, signal?: AbortSignal): Promise<void> {\n if (count > this.capacity) {\n throw new RateLimitError(\n `Requested ${count} tokens exceeds capacity ${this.capacity}`,\n Infinity,\n 'token-bucket',\n this.capacity,\n count\n );\n }\n while (true) {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n const deficit = count - this.tokens;\n const waitMs = Math.ceil((deficit / this.refillRate) * this.refillInterval);\n await abortableDelay(waitMs, signal);\n }\n }\n\n /** Alias for `acquireOrThrow()` — satisfies the `Limiter` interface. */\n acquire(count = 1): void {\n this.acquireOrThrow(count);\n }\n\n /** Satisfies the `Limiter` interface — alias for `tryConsume()`. */\n tryAcquire(): boolean {\n return this.tryConsume();\n }\n\n /** Satisfies the `Limiter` interface — alias for `consume()`. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n return this.consume(1, signal);\n }\n\n /** Throws immediately if tokens are unavailable (non-blocking equivalent of `consume`). */\n acquireOrThrow(count = 1): void {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n const deficit = count - this.tokens;\n const retryAfterMs = Math.ceil((deficit / this.refillRate) * this.refillInterval);\n throw new RateLimitError(\n `Rate limit exceeded. Retry after ${retryAfterMs}ms`,\n retryAfterMs,\n 'token-bucket',\n this.capacity,\n Math.floor(this.tokens)\n );\n }\n\n /** Refill to full capacity and reset the refill clock. */\n reset(): void {\n this.tokens = this.capacity;\n this.lastRefillTime = Date.now();\n }\n\n /** Hot-resize capacity. Clamps current tokens if the new capacity is lower. */\n setCapacity(capacity: number): void {\n if (capacity < 1) throw new RangeError('capacity must be >= 1');\n this.capacity = capacity as typeof this.capacity;\n this.tokens = Math.min(this.tokens, capacity);\n }\n}\n\n// ─── Sliding Window ───────────────────────────────────────────────────────────\n\n/**\n * Sliding Window rate limiter.\n *\n * Tracks exact request timestamps in a ring buffer. Strict enforcement —\n * no burst beyond `maxRequests` regardless of spacing.\n *\n * - `tryAcquire()` — non-throwing.\n * - `acquire()` — throws `RateLimitError` immediately.\n * - `waitAndAcquire()` — async, blocks until a slot is available.\n *\n * @example\n * const window = new SlidingWindow({ windowMs: 60_000, maxRequests: 100 });\n * await window.waitAndAcquire(); // queues caller until a slot opens\n */\nexport class SlidingWindow {\n // Ring buffer: pre-allocated array + head index avoids O(n) Array.shift()\n private readonly ring: Float64Array;\n private head = 0;\n private count = 0;\n private readonly windowMs: number;\n private readonly maxRequests: number;\n\n constructor(options: SlidingWindowOptions) {\n this.windowMs = options.windowMs;\n this.maxRequests = options.maxRequests;\n this.ring = new Float64Array(options.maxRequests);\n }\n\n private prune(): void {\n const cutoff = Date.now() - this.windowMs;\n while (this.count > 0 && this.ring[this.head % this.maxRequests] < cutoff) {\n this.head++;\n this.count--;\n }\n }\n\n get currentCount(): number {\n this.prune();\n return this.count;\n }\n\n tryAcquire(): boolean {\n this.prune();\n if (this.count < this.maxRequests) {\n this.ring[(this.head + this.count) % this.maxRequests] = Date.now();\n this.count++;\n return true;\n }\n return false;\n }\n\n acquire(): void {\n this.prune();\n if (this.count < this.maxRequests) {\n this.ring[(this.head + this.count) % this.maxRequests] = Date.now();\n this.count++;\n return;\n }\n const oldest = this.ring[this.head % this.maxRequests];\n const retryAfterMs = Math.max(0, oldest + this.windowMs - Date.now());\n throw new RateLimitError(\n `Rate limit exceeded. ${this.maxRequests} requests per ${this.windowMs}ms window.`,\n retryAfterMs,\n 'sliding-window',\n this.maxRequests,\n this.count\n );\n }\n\n /** Blocks until a slot is available, then acquires it. Cancellable via AbortSignal. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n const oldest = this.ring[this.head % this.maxRequests];\n const waitMs = Math.max(1, oldest + this.windowMs - Date.now());\n await abortableDelay(waitMs, signal);\n }\n }\n}\n\n// ─── Fixed Window ─────────────────────────────────────────────────────────────\n\n/**\n * Fixed Window rate limiter.\n *\n * Simplest algorithm — counts requests in a fixed time bucket that resets\n * every `windowMs`. Easiest to reason about; susceptible to boundary spikes.\n *\n * @example\n * const fw = new FixedWindow({ windowMs: 60_000, maxRequests: 100 });\n * fw.acquire(); // throws if over limit in current window\n */\nexport class FixedWindow {\n private count = 0;\n private windowStart: number;\n private readonly windowMs: number;\n private readonly maxRequests: number;\n private readonly onWindowReset?: (windowStart: number) => void;\n\n constructor(options: FixedWindowOptions) {\n this.windowMs = options.windowMs;\n this.maxRequests = options.maxRequests;\n this.onWindowReset = options.onWindowReset;\n this.windowStart = Date.now();\n }\n\n private tick(): void {\n const now = Date.now();\n if (now - this.windowStart >= this.windowMs) {\n this.count = 0;\n this.windowStart = now;\n this.onWindowReset?.(this.windowStart);\n }\n }\n\n get currentCount(): number {\n this.tick();\n return this.count;\n }\n\n /** Ms until the current window resets. */\n get windowResetMs(): number {\n return Math.max(0, this.windowStart + this.windowMs - Date.now());\n }\n\n tryAcquire(): boolean {\n this.tick();\n if (this.count < this.maxRequests) {\n this.count++;\n return true;\n }\n return false;\n }\n\n acquire(): void {\n this.tick();\n if (this.count < this.maxRequests) {\n this.count++;\n return;\n }\n throw new RateLimitError(\n `Fixed window rate limit exceeded. Retry after ${this.windowResetMs}ms`,\n this.windowResetMs,\n 'fixed-window',\n this.maxRequests,\n this.count\n );\n }\n\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n await abortableDelay(this.windowResetMs || 1, signal);\n }\n }\n\n /** Manually reset the window (useful in tests). */\n reset(): void {\n this.count = 0;\n this.windowStart = Date.now();\n this.onWindowReset?.(this.windowStart);\n }\n}\n\n// ─── Composite Limiter ────────────────────────────────────────────────────────\n\n/**\n * Composite rate limiter — enforces multiple limits simultaneously.\n *\n * All limiters must pass for a call to be allowed. Useful when APIs enforce\n * multiple tiers (e.g. 10/second AND 500/minute AND 5000/hour).\n *\n * @example\n * const limiter = new CompositeLimiter([\n * new TokenBucket({ capacity: 10, refillRate: 10, refillInterval: 1000 }),\n * new SlidingWindow({ windowMs: 60_000, maxRequests: 500 }),\n * ]);\n * await limiter.waitAndAcquire();\n */\nexport class CompositeLimiter {\n constructor(private readonly limiters: Limiter[]) {\n if (limiters.length === 0) throw new RangeError('CompositeLimiter requires at least one limiter');\n }\n\n /** Returns `true` only if ALL limiters can acquire. Rolls back on partial failure. */\n tryAcquire(): boolean {\n const acquired: Limiter[] = [];\n for (const l of this.limiters) {\n if (l.tryAcquire()) {\n acquired.push(l);\n } else {\n // Rollback is not possible for SlidingWindow/FixedWindow after tryAcquire succeeds,\n // but we stop here — already-acquired counts against the window, which is acceptable.\n return false;\n }\n }\n return true;\n }\n\n /** Throws `RateLimitError` with the most restrictive `retryAfterMs`. */\n acquire(): void {\n for (const l of this.limiters) {\n l.acquire();\n }\n }\n\n /** Waits for ALL limiters to have capacity, then acquires atomically. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (true) {\n if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');\n\n // Try to acquire all — short circuit on first failure\n const acquired: Limiter[] = [];\n let waitMs = 0;\n\n let allPassed = true;\n for (const l of this.limiters) {\n if (l.tryAcquire()) {\n acquired.push(l);\n } else {\n allPassed = false;\n // Compute how long this limiter needs: use acquire() to get retryAfterMs\n // from the thrown RateLimitError; fall back to 1ms if unavailable.\n try {\n l.acquire();\n } catch (err) {\n if (err instanceof RateLimitError) {\n waitMs = Math.max(waitMs, err.retryAfterMs || 1);\n } else {\n waitMs = Math.max(waitMs, 1);\n }\n }\n break;\n }\n }\n\n if (allPassed) return;\n\n await abortableDelay(Math.max(1, waitMs), signal);\n }\n }\n}\n"]}
package/dist/index.mjs CHANGED
@@ -242,6 +242,7 @@ var FixedWindow = class {
242
242
  reset() {
243
243
  this.count = 0;
244
244
  this.windowStart = Date.now();
245
+ this.onWindowReset?.(this.windowStart);
245
246
  }
246
247
  };
247
248
  var CompositeLimiter = class {
@@ -266,9 +267,27 @@ var CompositeLimiter = class {
266
267
  }
267
268
  /** Waits for ALL limiters to have capacity, then acquires atomically. */
268
269
  async waitAndAcquire(signal) {
269
- while (!this.tryAcquire()) {
270
+ while (true) {
270
271
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
271
- await abortableDelay(1, signal);
272
+ let waitMs = 0;
273
+ let allPassed = true;
274
+ for (const l of this.limiters) {
275
+ if (l.tryAcquire()) ; else {
276
+ allPassed = false;
277
+ try {
278
+ l.acquire();
279
+ } catch (err) {
280
+ if (err instanceof RateLimitError) {
281
+ waitMs = Math.max(waitMs, err.retryAfterMs || 1);
282
+ } else {
283
+ waitMs = Math.max(waitMs, 1);
284
+ }
285
+ }
286
+ break;
287
+ }
288
+ }
289
+ if (allPassed) return;
290
+ await abortableDelay(Math.max(1, waitMs), signal);
272
291
  }
273
292
  }
274
293
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ratelimitx.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["export type { TokenBucketOptions, SlidingWindowOptions, FixedWindowOptions, Limiter, RateLimitAlgorithm } from './types.js';\nimport type { TokenBucketOptions, SlidingWindowOptions, FixedWindowOptions, Limiter } from './types.js';\n\nfunction abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) { reject(new DOMException('Aborted', 'AbortError')); return; }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); }, { once: true });\n });\n}\n\n// ─── Error ───────────────────────────────────────────────────────────────────\n\nexport class RateLimitError extends Error {\n constructor(\n message: string,\n public readonly retryAfterMs: number,\n public readonly algorithm: 'token-bucket' | 'sliding-window' | 'fixed-window',\n public readonly limit: number,\n public readonly current: number\n ) {\n super(message);\n this.name = 'RateLimitError';\n }\n}\n\n// ─── Shared Helpers ──────────────────────────────────────────────────────────\n\n/**\n * Token Bucket rate limiter.\n *\n * Tokens accumulate over time up to `capacity`. Burst-friendly — allows up\n * to `capacity` back-to-back calls as long as the bucket is full.\n *\n * - `tryConsume()` — non-throwing, returns `true` if tokens available.\n * - `consume()` — async, **blocks** until tokens are available.\n * - `available` — current token count (after refill).\n *\n * @example\n * const bucket = new TokenBucket({ capacity: 10, refillRate: 2, refillInterval: 1000 });\n * await bucket.consume(); // waits until a token is available\n */\nexport class TokenBucket {\n private tokens: number;\n private lastRefillTime: number;\n private capacity: number;\n private readonly refillRate: number;\n private readonly refillInterval: number;\n\n constructor(options: TokenBucketOptions) {\n this.capacity = options.capacity;\n this.refillRate = options.refillRate;\n this.refillInterval = options.refillInterval ?? 1000;\n this.tokens = options.capacity;\n this.lastRefillTime = Date.now();\n }\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefillTime;\n if (elapsed <= 0) return;\n const newTokens = (elapsed / this.refillInterval) * this.refillRate;\n if (newTokens >= 1) {\n // Advance lastRefillTime only by the consumed portion to preserve sub-interval leftovers\n const consumed = Math.floor(newTokens);\n this.tokens = Math.min(this.capacity, this.tokens + consumed);\n this.lastRefillTime += Math.floor(consumed * (this.refillInterval / this.refillRate));\n }\n }\n\n get available(): number {\n this.refill();\n return this.tokens;\n }\n\n tryConsume(count = 1): boolean {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return true;\n }\n return false;\n }\n\n /**\n * Waits until `count` tokens are available, then consumes them.\n * Throws `RateLimitError` only if the request can never be satisfied\n * (i.e. `count > capacity`). Cancellable via `AbortSignal`.\n */\n async consume(count = 1, signal?: AbortSignal): Promise<void> {\n if (count > this.capacity) {\n throw new RateLimitError(\n `Requested ${count} tokens exceeds capacity ${this.capacity}`,\n Infinity,\n 'token-bucket',\n this.capacity,\n count\n );\n }\n while (true) {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n const deficit = count - this.tokens;\n const waitMs = Math.ceil((deficit / this.refillRate) * this.refillInterval);\n await abortableDelay(waitMs, signal);\n }\n }\n\n /** Alias for `acquireOrThrow()` — satisfies the `Limiter` interface. */\n acquire(count = 1): void {\n this.acquireOrThrow(count);\n }\n\n /** Satisfies the `Limiter` interface — alias for `tryConsume()`. */\n tryAcquire(): boolean {\n return this.tryConsume();\n }\n\n /** Satisfies the `Limiter` interface — alias for `consume()`. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n return this.consume(1, signal);\n }\n\n /** Throws immediately if tokens are unavailable (non-blocking equivalent of `consume`). */\n acquireOrThrow(count = 1): void {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n const deficit = count - this.tokens;\n const retryAfterMs = Math.ceil((deficit / this.refillRate) * this.refillInterval);\n throw new RateLimitError(\n `Rate limit exceeded. Retry after ${retryAfterMs}ms`,\n retryAfterMs,\n 'token-bucket',\n this.capacity,\n Math.floor(this.tokens)\n );\n }\n\n /** Refill to full capacity and reset the refill clock. */\n reset(): void {\n this.tokens = this.capacity;\n this.lastRefillTime = Date.now();\n }\n\n /** Hot-resize capacity. Clamps current tokens if the new capacity is lower. */\n setCapacity(capacity: number): void {\n if (capacity < 1) throw new RangeError('capacity must be >= 1');\n this.capacity = capacity as typeof this.capacity;\n this.tokens = Math.min(this.tokens, capacity);\n }\n}\n\n// ─── Sliding Window ───────────────────────────────────────────────────────────\n\n/**\n * Sliding Window rate limiter.\n *\n * Tracks exact request timestamps in a ring buffer. Strict enforcement —\n * no burst beyond `maxRequests` regardless of spacing.\n *\n * - `tryAcquire()` — non-throwing.\n * - `acquire()` — throws `RateLimitError` immediately.\n * - `waitAndAcquire()` — async, blocks until a slot is available.\n *\n * @example\n * const window = new SlidingWindow({ windowMs: 60_000, maxRequests: 100 });\n * await window.waitAndAcquire(); // queues caller until a slot opens\n */\nexport class SlidingWindow {\n // Ring buffer: pre-allocated array + head index avoids O(n) Array.shift()\n private readonly ring: Float64Array;\n private head = 0;\n private count = 0;\n private readonly windowMs: number;\n private readonly maxRequests: number;\n\n constructor(options: SlidingWindowOptions) {\n this.windowMs = options.windowMs;\n this.maxRequests = options.maxRequests;\n this.ring = new Float64Array(options.maxRequests);\n }\n\n private prune(): void {\n const cutoff = Date.now() - this.windowMs;\n while (this.count > 0 && this.ring[this.head % this.maxRequests] < cutoff) {\n this.head++;\n this.count--;\n }\n }\n\n get currentCount(): number {\n this.prune();\n return this.count;\n }\n\n tryAcquire(): boolean {\n this.prune();\n if (this.count < this.maxRequests) {\n this.ring[(this.head + this.count) % this.maxRequests] = Date.now();\n this.count++;\n return true;\n }\n return false;\n }\n\n acquire(): void {\n this.prune();\n if (this.count < this.maxRequests) {\n this.ring[(this.head + this.count) % this.maxRequests] = Date.now();\n this.count++;\n return;\n }\n const oldest = this.ring[this.head % this.maxRequests];\n const retryAfterMs = Math.max(0, oldest + this.windowMs - Date.now());\n throw new RateLimitError(\n `Rate limit exceeded. ${this.maxRequests} requests per ${this.windowMs}ms window.`,\n retryAfterMs,\n 'sliding-window',\n this.maxRequests,\n this.count\n );\n }\n\n /** Blocks until a slot is available, then acquires it. Cancellable via AbortSignal. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n const oldest = this.ring[this.head % this.maxRequests];\n const waitMs = Math.max(1, oldest + this.windowMs - Date.now());\n await abortableDelay(waitMs, signal);\n }\n }\n}\n\n// ─── Fixed Window ─────────────────────────────────────────────────────────────\n\n/**\n * Fixed Window rate limiter.\n *\n * Simplest algorithm — counts requests in a fixed time bucket that resets\n * every `windowMs`. Easiest to reason about; susceptible to boundary spikes.\n *\n * @example\n * const fw = new FixedWindow({ windowMs: 60_000, maxRequests: 100 });\n * fw.acquire(); // throws if over limit in current window\n */\nexport class FixedWindow {\n private count = 0;\n private windowStart: number;\n private readonly windowMs: number;\n private readonly maxRequests: number;\n private readonly onWindowReset?: (windowStart: number) => void;\n\n constructor(options: FixedWindowOptions) {\n this.windowMs = options.windowMs;\n this.maxRequests = options.maxRequests;\n this.onWindowReset = options.onWindowReset;\n this.windowStart = Date.now();\n }\n\n private tick(): void {\n const now = Date.now();\n if (now - this.windowStart >= this.windowMs) {\n this.count = 0;\n this.windowStart = now;\n this.onWindowReset?.(this.windowStart);\n }\n }\n\n get currentCount(): number {\n this.tick();\n return this.count;\n }\n\n /** Ms until the current window resets. */\n get windowResetMs(): number {\n return Math.max(0, this.windowStart + this.windowMs - Date.now());\n }\n\n tryAcquire(): boolean {\n this.tick();\n if (this.count < this.maxRequests) {\n this.count++;\n return true;\n }\n return false;\n }\n\n acquire(): void {\n this.tick();\n if (this.count < this.maxRequests) {\n this.count++;\n return;\n }\n throw new RateLimitError(\n `Fixed window rate limit exceeded. Retry after ${this.windowResetMs}ms`,\n this.windowResetMs,\n 'fixed-window',\n this.maxRequests,\n this.count\n );\n }\n\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n await abortableDelay(this.windowResetMs || 1, signal);\n }\n }\n\n /** Manually reset the window (useful in tests). */\n reset(): void {\n this.count = 0;\n this.windowStart = Date.now();\n }\n}\n\n// ─── Composite Limiter ────────────────────────────────────────────────────────\n\n/**\n * Composite rate limiter — enforces multiple limits simultaneously.\n *\n * All limiters must pass for a call to be allowed. Useful when APIs enforce\n * multiple tiers (e.g. 10/second AND 500/minute AND 5000/hour).\n *\n * @example\n * const limiter = new CompositeLimiter([\n * new TokenBucket({ capacity: 10, refillRate: 10, refillInterval: 1000 }),\n * new SlidingWindow({ windowMs: 60_000, maxRequests: 500 }),\n * ]);\n * await limiter.waitAndAcquire();\n */\nexport class CompositeLimiter {\n constructor(private readonly limiters: Limiter[]) {\n if (limiters.length === 0) throw new RangeError('CompositeLimiter requires at least one limiter');\n }\n\n /** Returns `true` only if ALL limiters can acquire. Rolls back on partial failure. */\n tryAcquire(): boolean {\n const acquired: Limiter[] = [];\n for (const l of this.limiters) {\n if (l.tryAcquire()) {\n acquired.push(l);\n } else {\n // Rollback is not possible for SlidingWindow/FixedWindow after tryAcquire succeeds,\n // but we stop here — already-acquired counts against the window, which is acceptable.\n return false;\n }\n }\n return true;\n }\n\n /** Throws `RateLimitError` with the most restrictive `retryAfterMs`. */\n acquire(): void {\n for (const l of this.limiters) {\n l.acquire();\n }\n }\n\n /** Waits for ALL limiters to have capacity, then acquires atomically. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');\n await abortableDelay(1, signal);\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/ratelimitx.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["export type { TokenBucketOptions, SlidingWindowOptions, FixedWindowOptions, Limiter, RateLimitAlgorithm } from './types.js';\nimport type { TokenBucketOptions, SlidingWindowOptions, FixedWindowOptions, Limiter } from './types.js';\n\nfunction abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) { reject(new DOMException('Aborted', 'AbortError')); return; }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); }, { once: true });\n });\n}\n\n// ─── Error ───────────────────────────────────────────────────────────────────\n\nexport class RateLimitError extends Error {\n constructor(\n message: string,\n public readonly retryAfterMs: number,\n public readonly algorithm: 'token-bucket' | 'sliding-window' | 'fixed-window',\n public readonly limit: number,\n public readonly current: number\n ) {\n super(message);\n this.name = 'RateLimitError';\n }\n}\n\n// ─── Shared Helpers ──────────────────────────────────────────────────────────\n\n/**\n * Token Bucket rate limiter.\n *\n * Tokens accumulate over time up to `capacity`. Burst-friendly — allows up\n * to `capacity` back-to-back calls as long as the bucket is full.\n *\n * - `tryConsume()` — non-throwing, returns `true` if tokens available.\n * - `consume()` — async, **blocks** until tokens are available.\n * - `available` — current token count (after refill).\n *\n * @example\n * const bucket = new TokenBucket({ capacity: 10, refillRate: 2, refillInterval: 1000 });\n * await bucket.consume(); // waits until a token is available\n */\nexport class TokenBucket {\n private tokens: number;\n private lastRefillTime: number;\n private capacity: number;\n private readonly refillRate: number;\n private readonly refillInterval: number;\n\n constructor(options: TokenBucketOptions) {\n this.capacity = options.capacity;\n this.refillRate = options.refillRate;\n this.refillInterval = options.refillInterval ?? 1000;\n this.tokens = options.capacity;\n this.lastRefillTime = Date.now();\n }\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefillTime;\n if (elapsed <= 0) return;\n const newTokens = (elapsed / this.refillInterval) * this.refillRate;\n if (newTokens >= 1) {\n // Advance lastRefillTime only by the consumed portion to preserve sub-interval leftovers\n const consumed = Math.floor(newTokens);\n this.tokens = Math.min(this.capacity, this.tokens + consumed);\n this.lastRefillTime += Math.floor(consumed * (this.refillInterval / this.refillRate));\n }\n }\n\n get available(): number {\n this.refill();\n return this.tokens;\n }\n\n tryConsume(count = 1): boolean {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return true;\n }\n return false;\n }\n\n /**\n * Waits until `count` tokens are available, then consumes them.\n * Throws `RateLimitError` only if the request can never be satisfied\n * (i.e. `count > capacity`). Cancellable via `AbortSignal`.\n */\n async consume(count = 1, signal?: AbortSignal): Promise<void> {\n if (count > this.capacity) {\n throw new RateLimitError(\n `Requested ${count} tokens exceeds capacity ${this.capacity}`,\n Infinity,\n 'token-bucket',\n this.capacity,\n count\n );\n }\n while (true) {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n const deficit = count - this.tokens;\n const waitMs = Math.ceil((deficit / this.refillRate) * this.refillInterval);\n await abortableDelay(waitMs, signal);\n }\n }\n\n /** Alias for `acquireOrThrow()` — satisfies the `Limiter` interface. */\n acquire(count = 1): void {\n this.acquireOrThrow(count);\n }\n\n /** Satisfies the `Limiter` interface — alias for `tryConsume()`. */\n tryAcquire(): boolean {\n return this.tryConsume();\n }\n\n /** Satisfies the `Limiter` interface — alias for `consume()`. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n return this.consume(1, signal);\n }\n\n /** Throws immediately if tokens are unavailable (non-blocking equivalent of `consume`). */\n acquireOrThrow(count = 1): void {\n this.refill();\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n const deficit = count - this.tokens;\n const retryAfterMs = Math.ceil((deficit / this.refillRate) * this.refillInterval);\n throw new RateLimitError(\n `Rate limit exceeded. Retry after ${retryAfterMs}ms`,\n retryAfterMs,\n 'token-bucket',\n this.capacity,\n Math.floor(this.tokens)\n );\n }\n\n /** Refill to full capacity and reset the refill clock. */\n reset(): void {\n this.tokens = this.capacity;\n this.lastRefillTime = Date.now();\n }\n\n /** Hot-resize capacity. Clamps current tokens if the new capacity is lower. */\n setCapacity(capacity: number): void {\n if (capacity < 1) throw new RangeError('capacity must be >= 1');\n this.capacity = capacity as typeof this.capacity;\n this.tokens = Math.min(this.tokens, capacity);\n }\n}\n\n// ─── Sliding Window ───────────────────────────────────────────────────────────\n\n/**\n * Sliding Window rate limiter.\n *\n * Tracks exact request timestamps in a ring buffer. Strict enforcement —\n * no burst beyond `maxRequests` regardless of spacing.\n *\n * - `tryAcquire()` — non-throwing.\n * - `acquire()` — throws `RateLimitError` immediately.\n * - `waitAndAcquire()` — async, blocks until a slot is available.\n *\n * @example\n * const window = new SlidingWindow({ windowMs: 60_000, maxRequests: 100 });\n * await window.waitAndAcquire(); // queues caller until a slot opens\n */\nexport class SlidingWindow {\n // Ring buffer: pre-allocated array + head index avoids O(n) Array.shift()\n private readonly ring: Float64Array;\n private head = 0;\n private count = 0;\n private readonly windowMs: number;\n private readonly maxRequests: number;\n\n constructor(options: SlidingWindowOptions) {\n this.windowMs = options.windowMs;\n this.maxRequests = options.maxRequests;\n this.ring = new Float64Array(options.maxRequests);\n }\n\n private prune(): void {\n const cutoff = Date.now() - this.windowMs;\n while (this.count > 0 && this.ring[this.head % this.maxRequests] < cutoff) {\n this.head++;\n this.count--;\n }\n }\n\n get currentCount(): number {\n this.prune();\n return this.count;\n }\n\n tryAcquire(): boolean {\n this.prune();\n if (this.count < this.maxRequests) {\n this.ring[(this.head + this.count) % this.maxRequests] = Date.now();\n this.count++;\n return true;\n }\n return false;\n }\n\n acquire(): void {\n this.prune();\n if (this.count < this.maxRequests) {\n this.ring[(this.head + this.count) % this.maxRequests] = Date.now();\n this.count++;\n return;\n }\n const oldest = this.ring[this.head % this.maxRequests];\n const retryAfterMs = Math.max(0, oldest + this.windowMs - Date.now());\n throw new RateLimitError(\n `Rate limit exceeded. ${this.maxRequests} requests per ${this.windowMs}ms window.`,\n retryAfterMs,\n 'sliding-window',\n this.maxRequests,\n this.count\n );\n }\n\n /** Blocks until a slot is available, then acquires it. Cancellable via AbortSignal. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n const oldest = this.ring[this.head % this.maxRequests];\n const waitMs = Math.max(1, oldest + this.windowMs - Date.now());\n await abortableDelay(waitMs, signal);\n }\n }\n}\n\n// ─── Fixed Window ─────────────────────────────────────────────────────────────\n\n/**\n * Fixed Window rate limiter.\n *\n * Simplest algorithm — counts requests in a fixed time bucket that resets\n * every `windowMs`. Easiest to reason about; susceptible to boundary spikes.\n *\n * @example\n * const fw = new FixedWindow({ windowMs: 60_000, maxRequests: 100 });\n * fw.acquire(); // throws if over limit in current window\n */\nexport class FixedWindow {\n private count = 0;\n private windowStart: number;\n private readonly windowMs: number;\n private readonly maxRequests: number;\n private readonly onWindowReset?: (windowStart: number) => void;\n\n constructor(options: FixedWindowOptions) {\n this.windowMs = options.windowMs;\n this.maxRequests = options.maxRequests;\n this.onWindowReset = options.onWindowReset;\n this.windowStart = Date.now();\n }\n\n private tick(): void {\n const now = Date.now();\n if (now - this.windowStart >= this.windowMs) {\n this.count = 0;\n this.windowStart = now;\n this.onWindowReset?.(this.windowStart);\n }\n }\n\n get currentCount(): number {\n this.tick();\n return this.count;\n }\n\n /** Ms until the current window resets. */\n get windowResetMs(): number {\n return Math.max(0, this.windowStart + this.windowMs - Date.now());\n }\n\n tryAcquire(): boolean {\n this.tick();\n if (this.count < this.maxRequests) {\n this.count++;\n return true;\n }\n return false;\n }\n\n acquire(): void {\n this.tick();\n if (this.count < this.maxRequests) {\n this.count++;\n return;\n }\n throw new RateLimitError(\n `Fixed window rate limit exceeded. Retry after ${this.windowResetMs}ms`,\n this.windowResetMs,\n 'fixed-window',\n this.maxRequests,\n this.count\n );\n }\n\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (!this.tryAcquire()) {\n await abortableDelay(this.windowResetMs || 1, signal);\n }\n }\n\n /** Manually reset the window (useful in tests). */\n reset(): void {\n this.count = 0;\n this.windowStart = Date.now();\n this.onWindowReset?.(this.windowStart);\n }\n}\n\n// ─── Composite Limiter ────────────────────────────────────────────────────────\n\n/**\n * Composite rate limiter — enforces multiple limits simultaneously.\n *\n * All limiters must pass for a call to be allowed. Useful when APIs enforce\n * multiple tiers (e.g. 10/second AND 500/minute AND 5000/hour).\n *\n * @example\n * const limiter = new CompositeLimiter([\n * new TokenBucket({ capacity: 10, refillRate: 10, refillInterval: 1000 }),\n * new SlidingWindow({ windowMs: 60_000, maxRequests: 500 }),\n * ]);\n * await limiter.waitAndAcquire();\n */\nexport class CompositeLimiter {\n constructor(private readonly limiters: Limiter[]) {\n if (limiters.length === 0) throw new RangeError('CompositeLimiter requires at least one limiter');\n }\n\n /** Returns `true` only if ALL limiters can acquire. Rolls back on partial failure. */\n tryAcquire(): boolean {\n const acquired: Limiter[] = [];\n for (const l of this.limiters) {\n if (l.tryAcquire()) {\n acquired.push(l);\n } else {\n // Rollback is not possible for SlidingWindow/FixedWindow after tryAcquire succeeds,\n // but we stop here — already-acquired counts against the window, which is acceptable.\n return false;\n }\n }\n return true;\n }\n\n /** Throws `RateLimitError` with the most restrictive `retryAfterMs`. */\n acquire(): void {\n for (const l of this.limiters) {\n l.acquire();\n }\n }\n\n /** Waits for ALL limiters to have capacity, then acquires atomically. */\n async waitAndAcquire(signal?: AbortSignal): Promise<void> {\n while (true) {\n if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');\n\n // Try to acquire all — short circuit on first failure\n const acquired: Limiter[] = [];\n let waitMs = 0;\n\n let allPassed = true;\n for (const l of this.limiters) {\n if (l.tryAcquire()) {\n acquired.push(l);\n } else {\n allPassed = false;\n // Compute how long this limiter needs: use acquire() to get retryAfterMs\n // from the thrown RateLimitError; fall back to 1ms if unavailable.\n try {\n l.acquire();\n } catch (err) {\n if (err instanceof RateLimitError) {\n waitMs = Math.max(waitMs, err.retryAfterMs || 1);\n } else {\n waitMs = Math.max(waitMs, 1);\n }\n }\n break;\n }\n }\n\n if (allPassed) return;\n\n await abortableDelay(Math.max(1, waitMs), signal);\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@async-kit/ratelimitx",
3
- "version": "0.1.25",
3
+ "version": "0.2.0",
4
4
  "description": "Token bucket and sliding window rate limiter for JavaScript/TypeScript",
5
5
  "keywords": [
6
6
  "rate-limit",