用 JavaScript (ESM) 实现一个滑动窗口限流器,并配可自动运行的测试。要求:1) 导出 class RateLimiter(limit, windowMs);2) 方法 tryAcquire(nowMs) 返回布尔——窗口内已达 limit 次则返回 false,否则计数并返回 true;3) 时间一律由调用方传入 nowMs,内部绝不读系统时钟(可测试性硬要求);4) 窗口是滑动的:过期记录要被剔除,不是整窗清零;5) 用 node:test 写测试,至少覆盖:未达上限放行、达上限拒绝、窗口滑动后重新放行、边界时刻;6) 补齐 package.json 使其为 ESM 项目。不要引入任何第三方依赖。
. ├── package.json # ESM 项目配置("type": "module") ├── rate-limiter.js # RateLimiter 核心实现 ├── rate-limiter.test.js # node:test 测试(22 条用例) └── DELIVERY.md # 本文件
node --test
零依赖,不需要 npm install。沙箱里有 Node.js 就能跑。
import { RateLimiter } from './rate-limiter.js';
// 每 1000ms 最多允许 5 次请求
const limiter = new RateLimiter(5, 1000);
// 时间由调用方传入(通常用 Date.now())
if (limiter.tryAcquire(Date.now())) {
// 放行,处理请求
} else {
// 被限流,拒绝或等待
}new RateLimiter(limit, windowMs)limit — 窗口内最多允许通过的次数,必须为正有限整数windowMs — 滑动窗口宽度(毫秒),必须为正有限数TypeErrortryAcquire(nowMs): booleannowMs — 当前时间戳(毫秒),由调用方提供,内部绝不读取系统时钟true:未达上限,放行并记录本次时间戳false:窗口内已达上限,拒绝(不记录、不抛异常)nowMs 为非有限数字(NaN/±Infinity)时返回 false,不计数used — 当前窗口内已使用配额数remaining — 当前窗口内剩余配额数窗口是左开右闭区间 (nowMs - windowMs, nowMs]。淘汰条件为 timestamp <= nowMs - windowMs(早于或等于左边界的记录被清除)。
这意味着:
t=0 获得的许可,在 t=windowMs 时恰好过期t=windowMs 同一时刻可重新获得许可| 分类 | 覆盖点 |
|---|---|
| 构造参数 | 正常构造、limit/windowMs 非法值校验 |
| 未达上限放行 | 连续放行至满额、used/remaining 一致性、limit=1 极端情况 |
| 达上限拒绝 | 满额后拒绝、窗口未滑动前持续拒绝、limit=1 第二次即拒 |
| 窗口滑动后重新放行 | 单条过期释放一个名额、逐条滑动释放、全部过期窗口清空 |
| 边界时刻 | 右边界恰好过期、threshold 等于/大于/小于记录时间戳、nowMs=0、相同时间戳多次 acquire、非有限数字入参、时间倒退行为 |
| 综合场景 | 长时间多轮滑动、大窗口(86,400,000ms 一天)行为 |
全部 22 条测试通过,0 失败,0 跳过。
一个纯 JavaScript (ESM) 实现的滑动窗口限流器,内部维护请求时间戳队列,窗口随 nowMs 推进而平滑滑动——过期的记录被逐个剔除,而非整窗清零。
. ├── package.json # ESM 项目配置("type": "module") ├── rate-limiter.js # RateLimiter 核心实现 └── rate-limiter.test.js # node:test 测试套件
node --test
> 零依赖,全新 node --test 直接跑通。
new RateLimiter(limit, windowMs)| 参数 | 类型 | 说明 |
|---|---|---|
limitnumber |
| 窗口内允许的最大请求次数,必须为正整数 |
windowMs | number | 滑动窗口大小(毫秒),必须为正数 |
参数非法时抛出 TypeError。
rateLimiter.tryAcquire(nowMs)| 参数 | 类型 | 说明 |
|---|---|---|
nowMs | number | 当前时间戳(毫秒),由调用方传入 |
返回 boolean:
true — 未达上限,本次请求被记录并放行false — 窗口内已达 limit 上限,拒绝> 关键设计:内部绝不读取系统时钟,时间完全由调用方通过 nowMs 传入,确保可测试性和确定性。
rateLimiter.getCount(nowMs)返回当前窗口内的请求记录数(剔除过期记录后)。
rateLimiter.reset()清空所有请求记录,恢复初始状态。
export class RateLimiter {
#windowMs;
#limit;
#timestamps = [];
constructor(limit, windowMs) {
if (!Number.isFinite(limit) || limit <= 0 || !Number.isInteger(limit)) {
throw new TypeError('limit 必须为正整数');
}
if (!Number.isFinite(windowMs) || windowMs <= 0) {
throw new TypeError('windowMs 必须为正数');
}
this.#limit = limit;
this.#windowMs = windowMs;
this.#timestamps = [];
}
tryAcquire(nowMs) {
if (!Number.isFinite(nowMs)) {
throw new TypeError('nowMs 必须为有限数值');
}
const windowStart = nowMs - this.#windowMs;
const timestamps = this.#timestamps;
// 剔除窗口外的过期记录(指针 + splice,避免逐个 shift 的 O(n) 重排)
let validStart = 0;
while (validStart < timestamps.length && timestamps[validStart] < windowStart) {
validStart++;
}
if (validStart > 0) {
timestamps.splice(0, validStart);
}
if (timestamps.length >= this.#limit) {
return false;
}
timestamps.push(nowMs);
return true;
}
}tryAcquire 先找到第一个落在窗口内的记录索引,再对过期部分做一次性 splice——时间 O(k)(k = 过期记录数),避免每次 shift() 的数组整体前移。[nowMs - windowMs, nowMs],左闭右闭。即 nowMs - windowMs 时刻的请求仍在窗口内,nowMs - windowMs - 1 时刻的请求已滑出。nowMs,无任何 Date.now() / performance.now() 调用。测试可以模拟任意时间线、跳过真实等待。使用 node:test + node:assert/strict,覆盖以下场景:
| 场景 | 测试用例 |
|---|---|
| 未达上限放行 | limit=3 前 3 次放行、单次放行、limit=1 首次放行 |
| 达上限拒绝 | limit=2 第 3 次拒绝、limit=1 第 2 次拒绝、满窗持续拒绝 |
| 窗口滑动后重新放行 | 最早请求过期后放行、逐个滑出逐个放行、全部过期恢复满额 |
| 边界时刻 | 左边界记录保留(nowMs - windowMs 恰在窗口内)、边界滑出、密集请求边界表现 |
| 参数校验 | limit 非正整数拒绝、windowMs 非正数拒绝、nowMs 非有限值拒绝 |
| 不依赖系统时钟 | 任意绝对时间戳正常运作 |
| 辅助方法 | getCount 正确计数并剔除过期、reset 清空恢复 |
共 19 个测试用例,全部通过。