64 lines
2.9 KiB
Markdown
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.
|