2.9 KiB
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
windowMsare discarded on every check. isAllowedprobe — non-recording check for UX-level pre-flight.getChannelRemaining/getUserRemainingfor headers likeX-RateLimit-Remaining.- Injectable
Clockso tests advance time without sleeping. - Universal — no Node/DOM-specific APIs.
Usage: Basic
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
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 (orInfinitywhen 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.