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

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 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

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 (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.