Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,68 @@
# fixed-window
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the fixed-window rate-limiting algorithm. Time is divided into equal-length buckets of `windowMs` milliseconds. A counter per key tracks requests in the current bucket and resets to zero when a new bucket starts. This is the simplest algorithm — O(1) memory per key — suitable for coarse-grained limits where occasional boundary spikes (a burst at the end of one window plus the start of the next) are acceptable.
## Features
- O(1) counter per key with automatic window boundary reset
- Injectable `Clock` for deterministic tests
- `reset(key)` for admin and test operations
- `retryAfter` equals time until the current window ends
- `resetIn` reflects exact milliseconds until the next window
## Usage: Basic Example
```ts
import { FixedWindow } from "@suaveplan/rate-limiter";
const fw = new FixedWindow({ limit: 100, windowMs: 60_000 });
const result = fw.check("tenant:acme");
console.log(`${result.remaining} requests remaining`);
```
## Usage: Advanced Example
```ts
import { FixedWindow } from "@suaveplan/rate-limiter";
let t = 0;
const clock = () => t;
const fw = new FixedWindow({ limit: 2, windowMs: 1000, clock });
fw.check("k"); // count=1
fw.check("k"); // count=2
expect(fw.check("k").allowed).toBe(false);
// Advance to next window
t = 1000;
expect(fw.check("k").allowed).toBe(true); // counter reset
```
## API Reference
### `FixedWindow`
```ts
class FixedWindow {
constructor(options: FixedWindowOptions)
check(key: string): RateLimitResult
reset(key: string): void
}
```
### `FixedWindowOptions`
| Field | Type | Default | Description |
|---|---|---|---|
| `limit` | `number` | required | Max requests per window |
| `windowMs` | `number` | required | Window duration in milliseconds |
| `clock` | `Clock` | `Date.now` | Injectable time source |
## Implementation Notes
Window boundaries are computed as `Math.floor(now / windowMs) * windowMs`, so all callers sharing the same clock see synchronised window resets. The counter map retains stale entries indefinitely until `reset(key)` is called or the key reappears in a new window (at which point the stale entry is overwritten). This is safe because the window boundary check is always evaluated on access, never on eviction.

View File

@@ -0,0 +1,96 @@
# rate-limiter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createRateLimiter` is the top-level orchestrator for `@suaveplan/rate-limiter`. It accepts a `RateLimiterOptions` object and returns a `RateLimiterInstance` that applies rule-based algorithm routing, tenant-specific multipliers, persistent state via a pluggable `RateLimitStorage` adapter, OTel counters and histograms, and optional event-bus integration.
Rules are evaluated in descending priority order — the first matching rule wins. A bypass rule returns an `allowed: true` result immediately without touching storage. If no rule matches, the `defaultConfig` is applied. Tenant multipliers scale the effective limit for matching `tenantId` values.
## Features
- Rule-based routing with priority ordering and five condition operators
- Bypass rules for privileged keys (admin accounts, monitoring agents)
- Tenant multipliers: `effectiveLimit = baseLimit * multiplier`
- Pluggable `RateLimitStorage` (defaults to in-memory LRU)
- `rate:limit:exceeded` and `rate:limit:reset` event bus emissions
- Structured logging via `LoggerLike`
- OTel: `rate_limiter.checks_total`, `rate_limiter.check_duration_ms`, `rate_limiter.exceeded_total`
## Usage: Basic Example
```ts
import { createRateLimiter } from "@suaveplan/rate-limiter";
const limiter = createRateLimiter({
defaultConfig: { limit: 100, window: 60_000 },
});
const result = await limiter.check({ key: "user:42" });
if (!result.allowed) {
console.log(`Rate limited. Retry after ${result.retryAfter}ms`);
}
```
## Usage: Advanced Example
```ts
import { createRateLimiter, MemoryStorage } from "@suaveplan/rate-limiter";
import { EventBus } from "@suaveplan/events";
const bus = new EventBus();
bus.on("rate:limit:exceeded", ({ key, resetIn }) => {
console.warn(`Key ${key} exceeded. Reset in ${resetIn}ms`);
});
const limiter = createRateLimiter({
defaultConfig: { limit: 100, window: 60_000 },
storage: new MemoryStorage({ maxSize: 50_000 }),
rules: [
{
id: "admin-bypass",
limit: 0,
window: 60_000,
priority: 100,
bypass: true,
conditions: [{ field: "userId", operator: "in", value: ["admin-1"] }],
},
{
id: "strict-api",
limit: 10,
window: 1000,
priority: 50,
conditions: [{ field: "path", operator: "matches", value: "^/api/sensitive" }],
},
],
enableTenantLimits: true,
tenantMultipliers: { enterprise: 5 },
bus,
keyPrefix: "rl",
});
await limiter.reset("user:42");
const state = await limiter.getState("user:42");
```
## API Reference
### `createRateLimiter(options)`
Returns a `RateLimiterInstance`.
**Parameters**: `RateLimiterOptions` — see `rate-limiter.types.md`.
### `RateLimiterInstance`
| Method | Signature | Description |
|---|---|---|
| `check` | `(context: RateLimitContext) => Promise<RateLimitResult>` | Evaluate and record a request |
| `reset` | `(key: string) => Promise<void>` | Clear state and emit reset event |
| `getState` | `(key: string) => Promise<RateLimitState \| null>` | Read current persisted state |
## Implementation Notes
State synchronisation between the in-memory `SlidingWindow` instances and the storage adapter is done by loading persisted timestamps on each `check` and writing back after evaluation. This is correct for single-process deployments but not atomic for distributed Redis-backed setups — a Redis Lua script or atomic transaction would be required for strict distributed correctness. For in-memory (single-process) use, the `SlidingWindow` is the authoritative source and the storage adapter provides durability across restarts.

View File

@@ -0,0 +1,73 @@
# sliding-window
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the sliding-window rate-limiting algorithm. Each key maintains a sorted array of request timestamps. On every `check`, timestamps older than `now - windowMs` are pruned and the remaining count is compared against `limit`. This produces a smooth rate distribution without the periodic boundary spikes that plague fixed-window counters.
The sliding window trades memory (O(limit) timestamps per key) for accuracy. It is the default algorithm used by `createRateLimiter`.
## Features
- Per-key timestamp arrays with automatic expiry pruning
- Injectable `Clock` for deterministic tests
- `reset(key)` for admin and test operations
- `retryAfter` equals time until the oldest request in the window expires
## Usage: Basic Example
```ts
import { SlidingWindow } from "@suaveplan/rate-limiter";
const sw = new SlidingWindow({ limit: 5, windowMs: 1000 });
const result = sw.check("user:1");
if (!result.allowed) {
console.log(`Retry in ${result.retryAfter}ms`);
}
```
## Usage: Advanced Example
```ts
import { SlidingWindow } from "@suaveplan/rate-limiter";
let t = 1_000_000;
const clock = () => t;
const sw = new SlidingWindow({ limit: 3, windowMs: 1000, clock });
// Fill limit
sw.check("user:1");
sw.check("user:1");
sw.check("user:1");
expect(sw.check("user:1").allowed).toBe(false);
// Advance past window — all old entries expire
t += 1001;
expect(sw.check("user:1").allowed).toBe(true);
```
## API Reference
### `SlidingWindow`
```ts
class SlidingWindow {
constructor(options: SlidingWindowOptions)
check(key: string): RateLimitResult
reset(key: string): void
}
```
### `SlidingWindowOptions`
| Field | Type | Default | Description |
|---|---|---|---|
| `limit` | `number` | required | Max requests in `windowMs` |
| `windowMs` | `number` | required | Window duration in milliseconds |
| `clock` | `Clock` | `Date.now` | Injectable time source |
## Implementation Notes
Timestamps are stored in chronological order. The prune step uses a linear scan from the start — acceptable because the splice is O(k) where k is the number of expired entries. For very high-frequency keys or very large windows, a circular-buffer implementation would be preferable. The `check` call only records a timestamp when `allowed === true`, so rejected requests do not consume capacity.

View File

@@ -0,0 +1,75 @@
# token-bucket
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the token bucket rate-limiting algorithm. Tokens accumulate continuously at `refillRate` tokens per millisecond up to `capacity`. Each `consume(n)` call checks whether enough tokens are available, deducts them if so, and returns a `RateLimitResult` indicating whether the request was allowed. Requests that arrive when the bucket is empty are rejected without penalising the bucket state.
The token bucket is ideal for workloads that benefit from burst tolerance: a burst of requests up to `capacity` is permitted as long as the long-run rate does not exceed `refillRate * 1000` requests per second.
## Features
- Continuous token refill based on elapsed wall-clock time
- Injectable `Clock` for deterministic tests without `sleep`
- `available` and `capacity` read-only getters
- Manual `refill(tokens?)` for test setup and admin operations
- `RateLimitResult` with `retryAfter` computed from deficit tokens
## Usage: Basic Example
```ts
import { TokenBucket } from "@suaveplan/rate-limiter";
const bucket = new TokenBucket({ capacity: 10, refillRate: 0.1 }); // 100 req/s
const result = bucket.consume();
if (!result.allowed) {
console.log(`Retry in ${result.retryAfter}ms`);
}
```
## Usage: Advanced Example
```ts
import { TokenBucket } from "@suaveplan/rate-limiter";
let t = Date.now();
const clock = () => t;
const bucket = new TokenBucket({ capacity: 5, refillRate: 0.005, clock });
// Drain the bucket
for (let i = 0; i < 5; i++) bucket.consume();
expect(bucket.available).toBe(0);
// Advance 1 second — refillRate 0.005 * 1000ms = 5 tokens
t += 1000;
expect(bucket.available).toBe(5);
```
## API Reference
### `TokenBucket`
```ts
class TokenBucket {
constructor(options: TokenBucketOptions)
consume(tokens?: number): RateLimitResult
refill(tokens?: number): void
readonly available: number
readonly capacity: number
}
```
### `TokenBucketOptions`
| Field | Type | Default | Description |
|---|---|---|---|
| `capacity` | `number` | required | Max tokens the bucket can hold |
| `refillRate` | `number` | required | Tokens added per millisecond |
| `initialTokens` | `number` | `capacity` | Starting token count |
| `clock` | `Clock` | `Date.now` | Injectable time source |
## Implementation Notes
Refill is performed lazily on every `consume()` and on the `available` getter. The `performRefill` helper uses `Math.min(capacity, available + elapsed * rate)` to clamp at capacity. The `retryAfter` in a rejected result is the ceiling of `deficit / refillRate`, giving the caller a precise time to retry.

View File

@@ -0,0 +1,97 @@
# rate-limiter-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the structured error class and stable error-code registry for `@suaveplan/rate-limiter`. All runtime failures in this package throw a `RateLimiterError`, which extends `SuaveplanError` from `@suaveplan/error`, ensuring a consistent error shape with code, context, cause-chain traversal, OTel span-event adapter, and i18n message resolution across the entire Suaveplan.
Using `defineCodes("RATE", [...])` pins every error identifier to a fully-qualified string such as `"RATE.STORAGE_ERROR"` that survives tree-shaking, minification, and code-split boundaries — callers can discriminate errors by `error.code` without importing the original source file.
## Features
- Four stable error codes: `STORAGE_ERROR`, `INVALID_CONFIG`, `KEY_MISSING`, `STATE_CORRUPT`
- Full TypeScript narrowing via the `RateLimiterErrorCode` union type
- `instanceof RateLimiterError` works across ESM / CJS boundaries
- Cause-chain support via `options.cause`
- Structured context attachment via `options.context`
- Injectable clock for deterministic `timestamp` in tests
## Usage: Basic Example
```ts
import { RateLimiterError, RATE_CODES } from "@suaveplan/rate-limiter/errors";
throw new RateLimiterError(
RATE_CODES.STORAGE_ERROR,
"rate.storage.failed",
{ context: { key: "user:123" } }
);
```
## Usage: Advanced Example
```ts
import { RateLimiterError, RATE_CODES } from "@suaveplan/rate-limiter/errors";
import { walkCauses } from "@suaveplan/error";
try {
await storage.get("user:123");
} catch (err) {
throw new RateLimiterError(
RATE_CODES.STORAGE_ERROR,
"rate.storage.failed",
{
context: { key: "user:123", operation: "get" },
...(err instanceof Error ? { cause: err } : {}),
}
);
}
// Discriminate by code at a higher layer:
try {
await rateLimiter.check(ctx);
} catch (err) {
if (err instanceof RateLimiterError && err.code === RATE_CODES.KEY_MISSING) {
// handle missing key
}
for (const cause of walkCauses(err)) {
console.error(cause.message);
}
}
```
## API Reference
### `RATE_CODES`
`Record` mapping reason names to fully-qualified code strings:
| Key | Value |
|---|---|
| `STORAGE_ERROR` | `"RATE.STORAGE_ERROR"` |
| `INVALID_CONFIG` | `"RATE.INVALID_CONFIG"` |
| `KEY_MISSING` | `"RATE.KEY_MISSING"` |
| `STATE_CORRUPT` | `"RATE.STATE_CORRUPT"` |
### `RateLimiterErrorCode`
Union type of all values in `RATE_CODES`.
### `RateLimiterError`
Extends `SuaveplanError<RateLimiterErrorCode>`.
**Constructor**: `(code: RateLimiterErrorCode, messageKey: string, options?: SuaveplanErrorOptions) => RateLimiterError`
- `code` — one of the `RATE_CODES` values
- `messageKey` — i18n key resolved by the global message resolver
- `options.context` — structured key-value context attached to the error
- `options.cause` — underlying error preserved in the cause chain
- `options.traceId` — OTel trace correlation id
- `options.clock` — injectable time source (defaults to `Date.now`)
## Implementation Notes
`defineCodes` returns a frozen object, so `RATE_CODES` is immutable at runtime. The `RateLimiterError` constructor sets `this.name = "RateLimiterError"` explicitly so that serialised stack traces identify the class correctly even when the prototype chain is obscured by bundlers.

View File

@@ -0,0 +1,99 @@
# rate-limit-middleware
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides framework-agnostic middleware factory helpers that wrap `createRateLimiter` for easy integration into HTTP servers, API gateways, and custom request pipelines. All middleware functions accept `req: unknown` and return a `Promise<RateLimitResult>`, keeping them decoupled from any specific framework (Hono, Express, Fastify, Bun's native server, etc.).
The three exported helpers cover the most common patterns: a simple checker function, IP-based limiting, and route-specific per-path limits. For Hono-specific wiring, consumers can use a thin adapter that reads `result.allowed` and returns a `429` response.
## Features
- `createRateLimitChecker` — returns a `(context) => Promise<RateLimitResult>` wrapper
- `getRateLimitResult` — low-level direct storage+algorithm call
- `createIpRateLimiter` — IP-key extraction with configurable `keyExtractor`, `skip`, `onLimited`
- `createRouteRateLimiter` — per-route config map with route-prefix matching
- All factories accept an optional `skip` predicate to bypass limiting for health probes
- `onLimited` callback for telemetry, logging, or 429-response generation
## Usage: Basic Example
```ts
import { createRateLimitChecker } from "@suaveplan/rate-limiter";
const check = createRateLimitChecker({
defaultConfig: { limit: 100, window: 60_000 },
});
const result = await check({ key: "user:42" });
if (!result.allowed) {
return new Response("Too Many Requests", { status: 429 });
}
```
## Usage: Advanced Example
```ts
import { createIpRateLimiter, createRouteRateLimiter, MemoryStorage } from "@suaveplan/rate-limiter";
const storage = new MemoryStorage({ maxSize: 10_000 });
// IP-based limiter that skips health checks
const ipLimiter = createIpRateLimiter(
{ limit: 200, window: 60_000 },
storage,
{
keyExtractor: (req) => (req as Request).headers.get("x-forwarded-for") ?? "unknown",
skip: (req) => (req as Request).url.endsWith("/health"),
onLimited: (result) => console.warn("IP limited", result),
}
);
// Route-specific limits
const routeLimiter = createRouteRateLimiter(
{
"/api/auth/login": { limit: 5, window: 60_000 },
"/api/search": { limit: 50, window: 10_000 },
},
storage
);
const result = await ipLimiter(request);
if (!result.allowed) {
return new Response("Too Many Requests", { status: 429 });
}
```
## API Reference
### `createRateLimitChecker(options)`
`(options: RateLimiterOptions) => (context: RateLimitContext) => Promise<RateLimitResult>`
### `getRateLimitResult(context, storage, config)`
`(context: RateLimitContext, storage: RateLimitStorage, config: RateLimitConfig) => Promise<RateLimitResult>`
Low-level helper — creates a fresh `SlidingWindow` per call, restoring persisted state from storage.
### `createIpRateLimiter(config, storage?, middlewareOptions?)`
Returns `(req: unknown) => Promise<RateLimitResult>`.
### `createRouteRateLimiter(routes, storage?, middlewareOptions?)`
Returns `(req: unknown) => Promise<RateLimitResult>`. Matches route prefixes in insertion order.
### `RateLimiterMiddlewareOptions`
| Field | Type | Description |
|---|---|---|
| `keyExtractor?` | `(req: unknown) => string` | Extract key from request |
| `onLimited?` | `(result: RateLimitResult) => void` | Called on every check (use to send 429) |
| `skip?` | `(req: unknown) => boolean` | Skip limiting for matching requests |
## Implementation Notes
`createIpRateLimiter` and `createRouteRateLimiter` each construct one `createRateLimiter` instance per route/IP config at factory time, sharing a single storage adapter. Route matching in `createRouteRateLimiter` uses `key.startsWith(route) || key.includes(route)` — for production use with complex routing rules, consumers should provide a custom `keyExtractor` that returns the matched route prefix.

View File

@@ -0,0 +1,64 @@
# rate-limiter.schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides Zod v4 validation schemas for all configuration objects in `@suaveplan/rate-limiter`. These schemas enable runtime validation of user-supplied configuration, safe parsing of stored state from external sources, and documentation of valid value ranges. They are tree-shakeable — import only the schemas you need.
Non-serialisable fields (`clock`, `bus`, `logger`, `storage`) are intentionally omitted from `RateLimiterOptionsSchema` since they cannot be represented in Zod.
## Features
- `RateLimitConfigSchema` — validates `limit`, `window`, optional `id` and `description`
- `RateLimitConditionSchema` — validates five-operator condition objects
- `RateLimitRuleSchema` — extends config with `priority`, `conditions`, `bypass`
- `RateLimitContextSchema` — validates per-request context objects
- `RateLimiterOptionsSchema` — partial options schema for serialisable fields
- `TokenBucketOptionsSchema`, `SlidingWindowOptionsSchema`, `FixedWindowOptionsSchema` — algorithm options
- `MemoryStorageOptionsSchema` — storage options
## Usage: Basic Example
```ts
import { RateLimitConfigSchema } from "@suaveplan/rate-limiter/schemas";
const result = RateLimitConfigSchema.safeParse({ limit: 100, window: 60_000 });
if (!result.success) {
console.error(result.error.issues);
}
```
## Usage: Advanced Example
```ts
import {
RateLimiterOptionsSchema,
RateLimitRuleSchema,
} from "@suaveplan/rate-limiter/schemas";
const raw = JSON.parse(configJson);
const opts = RateLimiterOptionsSchema.parse(raw);
// Validate individual rules from an API payload:
const rules = z.array(RateLimitRuleSchema).parse(payload.rules);
```
## API Reference
| Schema | Validates |
|---|---|
| `RateLimitConfigSchema` | `{ limit, window, id?, description? }` |
| `RateLimitConditionSchema` | `{ field, operator, value }` |
| `RateLimitRuleSchema` | `RateLimitConfig + { priority?, conditions?, bypass? }` |
| `RateLimitContextSchema` | `{ key, tenantId?, userId?, ip?, path?, method?, metadata? }` |
| `RateLimiterOptionsSchema` | Serialisable subset of `RateLimiterOptions` |
| `TokenBucketOptionsSchema` | `{ capacity, refillRate, initialTokens? }` |
| `SlidingWindowOptionsSchema` | `{ limit, windowMs }` |
| `FixedWindowOptionsSchema` | `{ limit, windowMs }` |
| `MemoryStorageOptionsSchema` | `{ maxSize? }` |
## Implementation Notes
`z.record(z.string(), z.unknown())` is used for `metadata` and `tenantMultipliers` fields, which requires both key and value type arguments in Zod v4. All integer fields use `.int()` to prevent floating-point surprises in window calculations.

View File

@@ -0,0 +1,65 @@
# storage-adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `RateLimitStorage` interface — the pluggable persistence contract that decouples rate-limit state from any specific backend. All algorithm implementations and the `RateLimiterInstance` orchestrator depend only on this interface, making it possible to swap in Redis, SurrealDB, Postgres, or any key-value store without modifying core logic.
The interface is intentionally minimal: four async methods covering read, write, delete, and clear. The `ttl` parameter on `set` is advisory: conformant backends SHOULD honour it, but the algorithms are defensive against stale state.
## Features
- `get(key)` — returns `RateLimitState | null` (null means absent or expired)
- `set(key, state, ttl)` — persist with advisory TTL in milliseconds
- `delete(key)` — idempotent removal
- `clear()` — full eviction (tests and admin reset)
- Zero runtime code — pure interface definition
## Usage: Basic Example
```ts
import type { RateLimitStorage } from "@suaveplan/rate-limiter";
class RedisStorage implements RateLimitStorage {
async get(key: string) { /* ... */ return null; }
async set(key: string, state, ttl: number) { /* ... */ }
async delete(key: string) { /* ... */ }
async clear() { /* ... */ }
}
```
## Usage: Advanced Example
```ts
import { createRateLimiter } from "@suaveplan/rate-limiter";
import type { RateLimitStorage } from "@suaveplan/rate-limiter";
// Provide a Redis-backed storage for distributed rate limiting:
const storage: RateLimitStorage = new RedisStorage(redisClient);
const limiter = createRateLimiter({
defaultConfig: { limit: 1000, window: 60_000 },
storage,
});
const result = await limiter.check({ key: "tenant:acme:user:7" });
```
## API Reference
### `RateLimitStorage`
Interface with four methods:
| Method | Signature | Description |
|---|---|---|
| `get` | `(key: string) => Promise<RateLimitState \| null>` | Read state or null |
| `set` | `(key: string, state: RateLimitState, ttl: number) => Promise<void>` | Write with TTL |
| `delete` | `(key: string) => Promise<void>` | Remove key |
| `clear` | `() => Promise<void>` | Remove all entries |
## Implementation Notes
The `ttl` passed to `set` is derived by the caller from the remaining window duration. Backends that do not support native TTL (e.g. in-process Maps) should store the expiry timestamp alongside the state and filter it in `get`. The built-in `MemoryStorage` follows this pattern.

View File

@@ -0,0 +1,78 @@
# memory-storage
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MemoryStorage` is the built-in, zero-dependency `RateLimitStorage` adapter for `@suaveplan/rate-limiter`. It stores rate-limit state in a `Map` within the current process, using insertion-order iteration for LRU eviction and lazy TTL expiry checks on `get`. It is ideal for single-process servers, testing environments, and local development where distributed coordination is not required.
The injectable `Clock` enables fully deterministic unit tests by advancing time without sleeping.
## Features
- LRU eviction: when `maxSize` is exceeded the oldest key is removed
- Lazy TTL enforcement: expired entries are evicted on next `get` access
- Injectable `Clock` for deterministic time in tests
- `size` getter for observability
- Implements full `RateLimitStorage` interface (`get`, `set`, `delete`, `clear`)
## Usage: Basic Example
```ts
import { MemoryStorage } from "@suaveplan/rate-limiter";
const storage = new MemoryStorage({ maxSize: 5000 });
await storage.set("user:1", state, 60_000);
const loaded = await storage.get("user:1");
```
## Usage: Advanced Example
```ts
import { MemoryStorage } from "@suaveplan/rate-limiter";
import { createRateLimiter } from "@suaveplan/rate-limiter";
// Use a controlled clock for deterministic TTL tests:
let t = 1_700_000_000_000;
const clock = () => t;
const storage = new MemoryStorage({ maxSize: 100, clock });
const limiter = createRateLimiter({
defaultConfig: { limit: 10, window: 1000 },
storage,
clock,
});
// Advance time past TTL to verify state expiry:
await limiter.check({ key: "user:1" });
t += 2000; // advance past window
const state = await storage.get("rl:user:1");
// state is null — TTL expired
```
## API Reference
### `MemoryStorage`
```ts
class MemoryStorage implements RateLimitStorage {
constructor(options?: MemoryStorageOptions)
get(key: string): Promise<RateLimitState | null>
set(key: string, state: RateLimitState, ttl: number): Promise<void>
delete(key: string): Promise<void>
clear(): Promise<void>
readonly size: number
}
```
### `MemoryStorageOptions`
| Field | Type | Default | Description |
|---|---|---|---|
| `maxSize` | `number` | `10000` | Maximum keys before LRU eviction |
| `clock` | `Clock` | `Date.now` | Injectable time source |
## Implementation Notes
LRU is implemented via `Map` deletion-then-reinsertion on both `get` and `set`, exploiting the specification guarantee that `Map` iterates in insertion order. The `set` path evicts exactly one entry when the map exceeds `maxSize`, keeping the operation O(1). Expired entries are not proactively swept — they accumulate until accessed, which keeps the hot path simple but means `size` may temporarily include expired keys.

View File

@@ -0,0 +1,88 @@
# rate-limiter.types
## Purpose
Defines all shared TypeScript interfaces and type aliases used across the `@suaveplan/rate-limiter` package. Centralising types here avoids circular import cycles between implementation modules and ensures that every consumer — storage adapters, algorithm classes, middleware helpers, and Zod schemas — shares a single source of truth for type definitions.
The types are deliberately kept as pure interfaces with no runtime footprint. Every field that is optional uses the `readonly` modifier so accidental mutation at the call site is caught by the TypeScript compiler.
## Features
- `RateLimitResult` — the universal check result (allowed, remaining, resetIn, retryAfter)
- `RateLimitConfig` and `RateLimitRule` — static configuration shapes for algorithms and rule routing
- `RateLimitCondition` — field-level predicate for rule matching with five operators
- `RateLimitContext` — per-request context used for rule evaluation and key construction
- `RateLimiterOptions` — top-level orchestrator configuration
- Serialisable state interfaces: `TokenBucketState`, `SlidingWindowState`, `CounterState`, `RateLimitState`
- Event payload types: `RateLimitedPayload`, `RateLimitResetPayload`, `RateLimiterEvents`
- `RateLimiterInstance` — the returned facade from `createRateLimiter`
- `MiddlewareFn` and `RateLimiterMiddlewareOptions` — framework-agnostic middleware contracts
## Usage: Basic Example
```ts
import type { RateLimitResult, RateLimitContext } from "@suaveplan/rate-limiter";
const context: RateLimitContext = {
key: "user:42",
tenantId: "acme",
ip: "1.2.3.4",
};
const result: RateLimitResult = await rateLimiter.check(context);
if (!result.allowed) {
console.log(`Retry after ${result.retryAfter}ms`);
}
```
## Usage: Advanced Example
```ts
import type {
RateLimitRule,
RateLimitCondition,
RateLimiterOptions,
} from "@suaveplan/rate-limiter";
const adminBypassRule: RateLimitRule = {
limit: 0,
window: 60_000,
priority: 100,
bypass: true,
conditions: [
{ field: "userId", operator: "in", value: ["admin-1", "admin-2"] },
],
};
const opts: RateLimiterOptions = {
defaultConfig: { limit: 100, window: 60_000 },
rules: [adminBypassRule],
enableTenantLimits: true,
tenantMultipliers: { enterprise: 10 },
};
```
## API Reference
### `RateLimitResult`
| Field | Type | Description |
|---|---|---|
| `allowed` | `boolean` | Whether the request should proceed |
| `remaining` | `number` | Remaining capacity in the current window |
| `limit` | `number` | Effective limit applied |
| `resetIn` | `number` | Milliseconds until window/bucket resets |
| `retryAfter?` | `number` | Milliseconds to wait (only when `allowed === false`) |
| `metadata?` | `Record<string, unknown>` | Optional algorithm-specific metadata |
### `RateLimitConditionOperator`
`"eq" | "neq" | "in" | "nin" | "matches"` — five operators for field-based predicate evaluation.
### `RateLimitAlgorithm`
`"token-bucket" | "sliding-window" | "fixed-window"` — the three built-in algorithms.
## Implementation Notes
The `RateLimiterEvents` interface is intentionally typed with string literal keys (`"rate:limit:exceeded"`) so it can be passed directly to `EventBusLike<RateLimiterEvents>`. The storage adapter interface (`RateLimitStorage`) is defined in the `storage-adapter` module to avoid a circular dependency back into this types file.