Files
skills/docs/communication/messaging/notification-channel/src/rate-limiter/channel-rate-limiter.md
2026-05-29 08:20:56 +01:00

64 lines
2.9 KiB
Markdown

# `channel-rate-limiter` — Sliding-window rate limits
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.