feat(rate-limiter): add rate limiter package#131
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a new Changes
Sequence DiagramsequenceDiagram
actor Client
participant RateLimiter
participant Algorithm as TokenBucketAlgorithm
participant Storage as MemoryStorage
Client->>RateLimiter: check(ruleName, request)
RateLimiter->>RateLimiter: resolve rule, keyGenerator, storage
RateLimiter->>Algorithm: check(key, storage)
Algorithm->>Storage: get("<key>:tb:tokens")
Storage-->>Algorithm: tokens / null
Algorithm->>Storage: get("<key>:tb:lastRefill")
Storage-->>Algorithm: lastRefill / null
Algorithm->>Algorithm: compute refill based on elapsed time
alt tokens >= 1
Algorithm->>Storage: set("<key>:tb:tokens", decremented, ttl)
Algorithm-->>RateLimiter: RateLimitResult(ok: true, remaining,...)
else tokens < 1
Algorithm-->>RateLimiter: RateLimitResult(ok: false, retryAfter, resetAt)
end
RateLimiter-->>Client: RateLimitResult (caller may call toResponse())
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
packages/rate-limiter/src/types.ts (2)
34-45: StrengthenRateLimitResultwith a discriminated union.The docs say
retryAfterexists only when blocked, but the current type cannot enforce that. A union would prevent invalid states at compile-time.🔒 Type-safe result shape
-export interface RateLimitResult { - /** Whether the request is allowed to proceed. */ - allowed: boolean - /** Configured maximum for this rule. */ - limit: number - /** Requests / tokens remaining in the current window. */ - remaining: number - /** Unix timestamp (ms) when the window / bucket resets. */ - resetAt: number - /** Only present when `allowed` is false. Milliseconds to wait before retrying. */ - retryAfter?: number -} +export type RateLimitResult = + | { + /** Whether the request is allowed to proceed. */ + allowed: true + /** Configured maximum for this rule. */ + limit: number + /** Requests / tokens remaining in the current window. */ + remaining: number + /** Unix timestamp (ms) when the window / bucket resets. */ + resetAt: number + retryAfter?: never + } + | { + /** Whether the request is allowed to proceed. */ + allowed: false + /** Configured maximum for this rule. */ + limit: number + /** Requests / tokens remaining in the current window. */ + remaining: number + /** Unix timestamp (ms) when the window / bucket resets. */ + resetAt: number + /** Milliseconds to wait before retrying. */ + retryAfter: number + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/rate-limiter/src/types.ts` around lines 34 - 45, Replace the loose RateLimitResult interface with a discriminated union so the shape of the object is enforced by the allowed boolean: create two types (e.g., AllowedRateLimitResult and BlockedRateLimitResult) where AllowedRateLimitResult has allowed: true and no retryAfter, and BlockedRateLimitResult has allowed: false and a required retryAfter; then export RateLimitResult as the union of those two types and keep shared fields (limit, remaining, resetAt) common to both.
92-92: Allow asynconRejectedhandlers.
onRejectedcurrently only types sync returns. AllowingPromiseimproves integration with async frameworks/middleware flows.♻️ Small signature extension
- onRejected?: (result: RateLimitResult, endpoint: string) => unknown + onRejected?: (result: RateLimitResult, endpoint: string) => unknown | Promise<unknown>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/rate-limiter/src/types.ts` at line 92, Update the onRejected handler type to allow asynchronous handlers by changing its return type to include Promise (e.g., (result: RateLimitResult, endpoint: string) => unknown | Promise<unknown>); locate the onRejected declaration in types.ts and adjust the signature so callers can return either a sync value or a Promise (you may also use PromiseLike if preferred).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/rate-limiter/package.json`:
- Line 25: The publishConfig.registry value is incorrectly set to a
package-scoped path; update the publishConfig.registry entry to point to the
registry host URL "https://registry.npmjs.org/" instead of
"https://registry.npmjs.org/@aura-stack/rate-limiter" so npm sees the base
registry; locate the publishConfig.registry key in package.json and replace the
scoped URL with the base registry host.
- Line 31: The package.json currently sets "exports" to an empty object which
prevents any import/require; replace the empty "exports" with explicit entry
points mirroring sibling packages (e.g. expose the root "." to the built bundle
and include "./package.json"); specifically update the "exports" field so "."
maps to the package's built entry (both import and require keys if you ship ESM
+ CJS) and include "./package.json":"./package.json" so consumers and tooling
can read metadata — adjust references to your built files (dist/. ) to match the
actual output filenames used by this package.
In `@packages/rate-limiter/src/algorithms/token-bucket.ts`:
- Around line 22-24: Validate the TokenBucketRule inputs at the start of
createTokenBucketAlgorithm: ensure capacity and refillRatePerMs are finite
numbers > 0 (use Number.isFinite and > 0) and throw a clear error if not; this
prevents divide-by-zero and invalid ttlMs/resetAt/retryAfter calculations later
in functions that use capacity and refillRatePerMs (references: capacity,
refillRatePerMs, createTokenBucketAlgorithm). Apply the same validation/guard
logic wherever the code recalculates or relies on those values (the spots
computing ttlMs, resetAt, retryAfter) to either normalize values or fail fast
with informative errors.
- Around line 45-47: The current early-return resets state to full capacity when
either tokensEntry or refillEntry is missing; instead, handle the two partial
cases conservatively: if tokensEntry exists but refillEntry is missing, keep
tokensEntry.tokens (clamped to capacity) and set lastRefillAt to now (do not
refill to full); if refillEntry exists but tokensEntry is missing, compute
tokens by applying the elapsed time since refillEntry.lastRefillAt using the
refill rate (cap to capacity) and set lastRefillAt from refillEntry; only if
both are truly absent return { tokens: capacity, lastRefillAt: now }. Update the
code around tokensEntry/refillEntry checks to implement these branches and
ensure you reference capacity, lastRefillAt and now when clamping and computing
values.
In `@packages/rate-limiter/src/memory.ts`:
- Around line 54-65: The increment function currently increases existing.value
but doesn't refresh the TTL, causing active keys to expire; update the logic in
increment (and use the StorageEntry shape) so that when an existing entry is
found and not expired you set existing.expiresAt = now + ttlMs (and re-save it
via store.set(key, existing) if your store requires it) before returning the new
value; keep using isExpired to detect expired entries and preserve the existing
return behavior.
In `@packages/rate-limiter/src/rate-limiter.ts`:
- Around line 75-77: The reset implementation currently hardcodes token-bucket
key suffixes (`:tb:tokens`, `:tb:lastRefill`) which duplicates the naming
convention in the token-bucket algorithm and can break if names change; update
reset to import and call the canonical key helper(s) exported from
packages/rate-limiter/src/algorithms/token-bucket.ts (e.g., tokensKey(baseKey)
and lastRefillKey(baseKey) or a single keysFor(baseKey) helper) and pass those
returned keys into storage.delete instead of constructing `${key}:tb:...`, so
all key naming is centralized in the token-bucket module.
- Around line 82-85: The current fallback calls algorithm.check(key, storage)
which mutates quota; change the fallback so peek semantics are preserved by
using a non-mutating read-only check: either call a read-only variant (e.g.,
algorithm.check(key, storage, { consume: false }) or
algorithm.checkReadOnly(key, storage)) or pass a cloned/snapshot storage (e.g.,
const snapshot = storage.clone(); algorithm.check(key, snapshot)) so no state is
changed; update the algorithm/storage interfaces accordingly if needed and keep
references to algorithm.peek and algorithm.check in the change.
- Line 12: The thrown error message in rate-limiter.ts is missing the actual
algorithm name; update the throw to include the algorithm variable (e.g., throw
new Error(`Unknown algorithm: ${algorithm}`)) so the error shows the offending
value (use the actual local identifier if it's named differently, and stringify
if it can be non-string).
In `@packages/rate-limiter/src/types.ts`:
- Around line 62-67: BaseRule's keyGenerator is unreachable because public
methods check/reset/peek only accept a string key; update the RateLimiter API to
accept the request shape so rule-level key derivation can be used: modify the
signatures of check, reset, and peek (and any related type definitions) to
accept either key: string | unknown (or overloads check(key: string) and
check(req: unknown)) and internally resolve the effective key by calling the
rule's keyGenerator (or falling back to the provided string) before performing
rate-limit operations; ensure functions/methods named check, reset, peek and the
BaseRule type reference keyGenerator to locate where to inject the resolveKey
behavior.
---
Nitpick comments:
In `@packages/rate-limiter/src/types.ts`:
- Around line 34-45: Replace the loose RateLimitResult interface with a
discriminated union so the shape of the object is enforced by the allowed
boolean: create two types (e.g., AllowedRateLimitResult and
BlockedRateLimitResult) where AllowedRateLimitResult has allowed: true and no
retryAfter, and BlockedRateLimitResult has allowed: false and a required
retryAfter; then export RateLimitResult as the union of those two types and keep
shared fields (limit, remaining, resetAt) common to both.
- Line 92: Update the onRejected handler type to allow asynchronous handlers by
changing its return type to include Promise (e.g., (result: RateLimitResult,
endpoint: string) => unknown | Promise<unknown>); locate the onRejected
declaration in types.ts and adjust the signature so callers can return either a
sync value or a Promise (you may also use PromiseLike if preferred).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6ca7edd8-58bc-41d0-b670-6738d78785da
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (10)
packages/rate-limiter/README.mdpackages/rate-limiter/package.jsonpackages/rate-limiter/src/algorithms/index.tspackages/rate-limiter/src/algorithms/token-bucket.tspackages/rate-limiter/src/index.tspackages/rate-limiter/src/memory.tspackages/rate-limiter/src/rate-limiter.tspackages/rate-limiter/src/types.tspackages/rate-limiter/tsconfig.jsonpackages/rate-limiter/tsup.config.ts
- Add CHANGELOG.md file - Add deno.json - Add tests
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/rate-limiter/test/index.test.ts (1)
164-177: Consider clarifying the duplicate storage reference.Storage is passed both at the top level (
storage) on line 167 and within the rule on line 173. Based on the implementation inrate-limiter.ts, the rule-level storage takes precedence. If the intent is to testresetusing the limiter's storage, this works, but the duplicate declaration is potentially confusing.💡 Simplified configuration (optional)
const storage = createMemoryStorage() const limiter = createRateLimiter({ - storage, rules: { signIn: { algorithm: "token-bucket", capacity: 2, refillRate: 0.01, storage, keyGenerator: (req: Request) => req.headers.get("x-forwarded-for") ?? "unknown", }, }, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/rate-limiter/test/index.test.ts` around lines 164 - 177, Test passes the same storage twice (both to createRateLimiter and again inside rules.signIn), which is confusing because rule-level storage overrides the top-level storage; update the test to remove the duplicate by deleting the storage property inside the rules.signIn block (or alternatively use a different storage variable if you intend to test rule-specific storage), so that createMemoryStorage() is only provided once and reset/peek behavior clearly exercises the limiter's top-level storage used by createRateLimiter and the signIn rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/rate-limiter/src/types.ts`:
- Around line 22-26: The TypeScript contract for increment says TTL is set only
when the key does not yet exist but memory implementation currently resets
existing.expiresAt on every call; change the implementation in the increment
method in packages/rate-limiter/src/memory.ts so that when an entry for the
given key already exists you do NOT update existing.expiresAt (only create
expiresAt when creating a new entry), preserving the original expiration;
alternatively, if you prefer the current behavior, update the comment/signature
in types.ts (the increment declaration) to document that TTL is refreshed on
each increment—choose one and make the contract and the increment implementation
(or its doc) consistent.
In `@packages/rate-limiter/src/utils.ts`:
- Around line 6-11: The "Retry-After" header is being set using raw.retryAfter
(milliseconds); convert it to seconds per RFC 7231 before building the Headers
object: when constructing headers (the Headers instantiation in utils.ts where
"Retry-After" is set), replace raw.retryAfter.toString() with the value
converted to seconds (e.g., Math.ceil(raw.retryAfter / 1000).toString()) so the
"Retry-After" header is an integer number of seconds.
---
Nitpick comments:
In `@packages/rate-limiter/test/index.test.ts`:
- Around line 164-177: Test passes the same storage twice (both to
createRateLimiter and again inside rules.signIn), which is confusing because
rule-level storage overrides the top-level storage; update the test to remove
the duplicate by deleting the storage property inside the rules.signIn block (or
alternatively use a different storage variable if you intend to test
rule-specific storage), so that createMemoryStorage() is only provided once and
reset/peek behavior clearly exercises the limiter's top-level storage used by
createRateLimiter and the signIn rule.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 39eb0364-d7e5-46b5-90ce-3915794cc23c
📒 Files selected for processing (12)
deno.jsonpackages/rate-limiter/CHANGELOG.mdpackages/rate-limiter/deno.jsonpackages/rate-limiter/package.jsonpackages/rate-limiter/src/algorithms/token-bucket.tspackages/rate-limiter/src/memory.tspackages/rate-limiter/src/rate-limiter.tspackages/rate-limiter/src/types.tspackages/rate-limiter/src/utils.tspackages/rate-limiter/test/index.test.tspackages/rate-limiter/tsconfig.jsonpackages/rate-limiter/vitest.config.ts
✅ Files skipped from review due to trivial changes (5)
- deno.json
- packages/rate-limiter/CHANGELOG.md
- packages/rate-limiter/tsconfig.json
- packages/rate-limiter/package.json
- packages/rate-limiter/deno.json
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/rate-limiter/src/memory.ts
- packages/rate-limiter/src/rate-limiter.ts
Description
This pull request adds the new
@aura-stack/rate-limiterpackage to the monorepo. The package introduces a rate limiting system based on the token-bucket algorithm, providing a flexible and extensible foundation for controlling request flow across applications.The implementation includes an in-memory storage adapter for managing tokens, with a design that allows future support for external storage providers such as Redis, Upstash, KV stores, and other key-value systems.
The package follows the monorepo standards, including configuration and tooling setup:
vitest.configtsup.config.tsdeno.jsonCHANGELOG.mdAdditionally, multiple entry points are provided to improve tree-shaking and allow consumers to import only the required modules.
Usage
Direct Usage
Summary by CodeRabbit
New Features
@aura-stack/rate-limiterpackage with token-bucket rate limiting algorithm support.Tests
Chores