alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
77 lines (68 loc) • 2.26 kB
text/typescript
import { createMiddleware, type Middleware } from "alepha";
import {
DateTimeProvider,
type DurationLike,
} from "../providers/DateTimeProvider.ts";
export interface ThrottleOptions {
/**
* Max calls per window.
*/
rate: number;
/**
* Window duration.
*/
per: DurationLike;
}
/**
* Middleware that rate-controls handler execution using a token bucket.
*
* Excess calls are **delayed** until capacity is available — never rejected.
* Process-local (not distributed) — it cannot rate-limit across instances.
*
* **Limitation**: the token refill is time-based and re-checked only when a
* waiter wakes, so a burst of *concurrent* calls can wake in the same window
* and briefly exceed `rate`. Treat it as traffic smoothing, not a hard cap —
* do not rely on it to enforce a strict quota.
*
* **Use case**: protect an external API from your own traffic.
*
* ```typescript
* class PaymentController {
* charge = $action({
* use: [$throttle({ rate: 80, per: [1, "second"] })],
* handler: async ({ body }) => this.stripe.charges.create(body),
* });
* }
* ```
*/
export const $throttle = (options: ThrottleOptions): Middleware => {
return createMiddleware({
name: "$throttle",
options: options as unknown as Record<string, unknown>,
handler: ({ alepha, next }) => {
const dateTimeProvider = alepha.inject(DateTimeProvider);
const intervalMs = dateTimeProvider
.duration(options.per)
.asMilliseconds();
let tokens = options.rate;
let lastRefill = dateTimeProvider.nowMillis();
return async (...args) => {
const now = dateTimeProvider.nowMillis();
const elapsed = now - lastRefill;
const refill = Math.floor(elapsed / intervalMs) * options.rate;
if (refill > 0) {
tokens = Math.min(options.rate, tokens + refill);
lastRefill += Math.floor(elapsed / intervalMs) * intervalMs;
}
if (tokens <= 0) {
const waitMs = intervalMs - (now - lastRefill);
await dateTimeProvider.wait(waitMs);
tokens = options.rate;
lastRefill = dateTimeProvider.nowMillis();
}
tokens--;
return next(...args);
};
},
});
};