# `channel-rate-limiter` — Sliding-window rate limits _(no submodules)_ ## Purpose `channel-rate-limiter.ts` ships the `ChannelRateLimiter` class — a per-channel + per-user sliding-window rate limiter. Two independent buckets are maintained: a per-channel bucket (every send against that channel counts) and a per-user bucket (every send for that user counts regardless of channel). `checkAndRecord(channel, userId)` consults both, throws `RateLimitExceededError` on overflow, otherwise records the send and returns. ## Features - Per-channel and per-user limits with independent `{ maxCount, windowMs }` rules. - Sliding window — entries older than `windowMs` are discarded on every check. - `isAllowed` probe — non-recording check for UX-level pre-flight. - `getChannelRemaining` / `getUserRemaining` for headers like `X-RateLimit-Remaining`. - Injectable `Clock` so tests advance time without sleeping. - Universal — no Node/DOM-specific APIs. ## Usage: Basic ```ts import { ChannelRateLimiter } from "@suaveplan/notification-channel"; const limiter = new ChannelRateLimiter(); limiter.setChannelLimit("email", { maxCount: 100, windowMs: 60_000 }); limiter.setUserLimit("user-42", { maxCount: 5, windowMs: 60_000 }); try { limiter.checkAndRecord("email", "user-42"); } catch (err) { /* RateLimitExceededError */ } ``` ## Usage: Advanced ```ts const limiter = new ChannelRateLimiter(() => clock.now()); limiter.setChannelLimit("sms", { maxCount: 10, windowMs: 60_000 }); // Pre-flight without recording: if (!limiter.isAllowed("sms", userId)) { return reply.code(429).send({ remaining: limiter.getChannelRemaining("sms"), }); } limiter.checkAndRecord("sms", userId); ``` ## API Reference - `ChannelRateLimiter(clock?: Clock)` — constructor. - `setChannelLimit(channel, rule)`, `setUserLimit(userId, rule)` — install rules. - `removeChannelLimit(channel)`, `removeUserLimit(userId)` — drop rules and their windows. - `checkAndRecord(channel, userId)` — throws on overflow. - `isAllowed(channel, userId)` — boolean probe. - `getChannelRemaining(channel)`, `getUserRemaining(userId)` — current capacity (or `Infinity` when no rule). - `RateLimitRule` — `{ maxCount, windowMs }`. ## Implementation Notes The window is sliding, not fixed — entries are filtered against `now - timestamp < windowMs` on every check. This is more accurate than fixed buckets at the cost of `O(n)` filtering on every check where `n` is the number of entries in the window; for buckets with `maxCount` in the low hundreds this is negligible. `recordChannel` / `recordUser` are no-ops when no rule is installed, so the `WindowEntry` arrays stay empty when the bucket has no limit. The injected `Clock` only governs the limiter's view of "now" — wall-clock drift between server and limiter does not exist because the limiter never compares its timestamps to anything external.