@crewhaus/rate-limiter 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -11
- package/src/index.test.ts +37 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/rate-limiter",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-dimensional token-bucket / leaky-bucket rate limiter (per-tenant, per-provider, per-tool)",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"test": "bun test src"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@crewhaus/errors": "0.1.
|
|
15
|
+
"@crewhaus/errors": "0.1.3"
|
|
16
16
|
},
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Max Meier",
|
|
20
|
-
"email": "max@
|
|
21
|
-
"url": "https://
|
|
20
|
+
"email": "max@crewhaus.ai",
|
|
21
|
+
"url": "https://crewhaus.ai"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
@@ -30,12 +30,7 @@
|
|
|
30
30
|
"url": "https://github.com/crewhaus/factory/issues"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
|
-
"access": "
|
|
33
|
+
"access": "public"
|
|
34
34
|
},
|
|
35
|
-
"files": [
|
|
36
|
-
"src",
|
|
37
|
-
"README.md",
|
|
38
|
-
"LICENSE",
|
|
39
|
-
"NOTICE"
|
|
40
|
-
]
|
|
35
|
+
"files": ["src", "README.md", "LICENSE", "NOTICE"]
|
|
41
36
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
RateLimitError,
|
|
12
12
|
bucketKeyOf,
|
|
13
13
|
createRateLimiter,
|
|
14
|
+
tokenBucketAvailable,
|
|
14
15
|
} from "./index";
|
|
15
16
|
|
|
16
17
|
describe("rate-limiter — T1 token-bucket", () => {
|
|
@@ -18,15 +19,19 @@ describe("rate-limiter — T1 token-bucket", () => {
|
|
|
18
19
|
const buckets = new Map<string, BucketConfig>([
|
|
19
20
|
["tenant:t1", { kind: "token-bucket", capacity: 10, refillPerSec: 1 }],
|
|
20
21
|
]);
|
|
21
|
-
|
|
22
|
+
// Inject a frozen clock so the bucket's refill is computed against virtual
|
|
23
|
+
// time. With the real clock, the few ms between acquire and inspect refill
|
|
24
|
+
// a fraction of a token (1/sec), nudging `available` to ~5.05 — enough to
|
|
25
|
+
// trip the old toBeCloseTo(5, 1) 0.05 tolerance on loaded CI runners.
|
|
26
|
+
// Frozen time makes the post-acquire balance exactly 5, deterministically.
|
|
27
|
+
const nowMs = 1_000_000;
|
|
28
|
+
const rl = createRateLimiter({ buckets, now: (): number => nowMs });
|
|
22
29
|
const t0 = Date.now();
|
|
23
30
|
await rl.acquire([{ dimension: "tenant", id: "t1" }], 5);
|
|
24
|
-
//
|
|
25
|
-
// refill rate, while tolerating CI scheduler jitter (we saw 51 ms flakes
|
|
26
|
-
// against a 50 ms cap on shared GitHub runners).
|
|
31
|
+
// "immediate": acquiring below capacity must not block (real wall-clock).
|
|
27
32
|
expect(Date.now() - t0).toBeLessThan(250);
|
|
28
33
|
const inspect = rl.inspect({ dimension: "tenant", id: "t1" });
|
|
29
|
-
expect(inspect?.available).
|
|
34
|
+
expect(inspect?.available).toBe(5);
|
|
30
35
|
});
|
|
31
36
|
|
|
32
37
|
test("burst tolerance: capacity available immediately at start", async () => {
|
|
@@ -177,3 +182,30 @@ describe("rate-limiter — bucketKeyOf", () => {
|
|
|
177
182
|
expect(bucketKeyOf(k)).toBe("provider:anthropic");
|
|
178
183
|
});
|
|
179
184
|
});
|
|
185
|
+
|
|
186
|
+
describe("rate-limiter — tokenBucketAvailable static helper", () => {
|
|
187
|
+
const config: BucketConfig = { kind: "token-bucket", capacity: 10, refillPerSec: 1 };
|
|
188
|
+
|
|
189
|
+
test("returns true when enough tokens are present without refill", () => {
|
|
190
|
+
// Fresh state at full capacity, now == lastRefill so no time elapses.
|
|
191
|
+
const state = { tokens: 5, lastRefillMs: 1_000 };
|
|
192
|
+
expect(tokenBucketAvailable(state, 5, 1_000, config)).toBe(true);
|
|
193
|
+
// Pure capacity check must not mutate the balance when no time passes.
|
|
194
|
+
expect(state.tokens).toBe(5);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("returns false when tokens are insufficient even after available refill", () => {
|
|
198
|
+
const state = { tokens: 0, lastRefillMs: 1_000 };
|
|
199
|
+
// 500ms later → 0.5 token refilled at 1/sec, still < cost of 5.
|
|
200
|
+
expect(tokenBucketAvailable(state, 5, 1_500, config)).toBe(false);
|
|
201
|
+
expect(state.tokens).toBeCloseTo(0.5, 5);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("refills (and mutates state) based on elapsed virtual time before comparing", () => {
|
|
205
|
+
const state = { tokens: 0, lastRefillMs: 1_000 };
|
|
206
|
+
// 6s later at 1/sec → 6 tokens refilled, now >= cost of 5.
|
|
207
|
+
expect(tokenBucketAvailable(state, 5, 7_000, config)).toBe(true);
|
|
208
|
+
expect(state.tokens).toBeCloseTo(6, 5);
|
|
209
|
+
expect(state.lastRefillMs).toBe(7_000);
|
|
210
|
+
});
|
|
211
|
+
});
|