@crewhaus/rate-limiter 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/package.json +6 -11
  2. 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.1",
3
+ "version": "0.1.2",
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.1"
15
+ "@crewhaus/errors": "0.1.2"
16
16
  },
17
17
  "license": "Apache-2.0",
18
18
  "author": {
19
19
  "name": "Max Meier",
20
- "email": "max@studiomax.io",
21
- "url": "https://studiomax.io"
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": "restricted"
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
- const rl = createRateLimiter({ buckets });
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
- // 250 ms threshold: "immediate" relative to the bucket's 1-token-per-second
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).toBeCloseTo(5, 1);
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
+ });