first commit
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user