@xyph3r/rate-limiter 0.1.0

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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +310 -0
  3. package/dist/adapters/express.d.ts +26 -0
  4. package/dist/adapters/express.d.ts.map +1 -0
  5. package/dist/adapters/express.js +51 -0
  6. package/dist/adapters/express.js.map +1 -0
  7. package/dist/adapters/fastify.d.ts +24 -0
  8. package/dist/adapters/fastify.d.ts.map +1 -0
  9. package/dist/adapters/fastify.js +40 -0
  10. package/dist/adapters/fastify.js.map +1 -0
  11. package/dist/adapters/fetch.d.ts +17 -0
  12. package/dist/adapters/fetch.d.ts.map +1 -0
  13. package/dist/adapters/fetch.js +53 -0
  14. package/dist/adapters/fetch.js.map +1 -0
  15. package/dist/adapters/hono.d.ts +25 -0
  16. package/dist/adapters/hono.d.ts.map +1 -0
  17. package/dist/adapters/hono.js +40 -0
  18. package/dist/adapters/hono.js.map +1 -0
  19. package/dist/adapters/nest.d.ts +32 -0
  20. package/dist/adapters/nest.d.ts.map +1 -0
  21. package/dist/adapters/nest.js +47 -0
  22. package/dist/adapters/nest.js.map +1 -0
  23. package/dist/adapters/next.d.ts +12 -0
  24. package/dist/adapters/next.d.ts.map +1 -0
  25. package/dist/adapters/next.js +11 -0
  26. package/dist/adapters/next.js.map +1 -0
  27. package/dist/core/cached-rate-limiter-proxy.d.ts +17 -0
  28. package/dist/core/cached-rate-limiter-proxy.d.ts.map +1 -0
  29. package/dist/core/cached-rate-limiter-proxy.js +47 -0
  30. package/dist/core/cached-rate-limiter-proxy.js.map +1 -0
  31. package/dist/core/create-rate-limiter.d.ts +11 -0
  32. package/dist/core/create-rate-limiter.d.ts.map +1 -0
  33. package/dist/core/create-rate-limiter.js +34 -0
  34. package/dist/core/create-rate-limiter.js.map +1 -0
  35. package/dist/core/rate-limiter-builder.d.ts +27 -0
  36. package/dist/core/rate-limiter-builder.d.ts.map +1 -0
  37. package/dist/core/rate-limiter-builder.js +73 -0
  38. package/dist/core/rate-limiter-builder.js.map +1 -0
  39. package/dist/core/rate-limiter.d.ts +23 -0
  40. package/dist/core/rate-limiter.d.ts.map +1 -0
  41. package/dist/core/rate-limiter.js +59 -0
  42. package/dist/core/rate-limiter.js.map +1 -0
  43. package/dist/core/strategy-factory.d.ts +18 -0
  44. package/dist/core/strategy-factory.d.ts.map +1 -0
  45. package/dist/core/strategy-factory.js +17 -0
  46. package/dist/core/strategy-factory.js.map +1 -0
  47. package/dist/errors.d.ts +7 -0
  48. package/dist/errors.d.ts.map +1 -0
  49. package/dist/errors.js +13 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/index.d.ts +19 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +17 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/stores/memory-store.d.ts +9 -0
  56. package/dist/stores/memory-store.d.ts.map +1 -0
  57. package/dist/stores/memory-store.js +24 -0
  58. package/dist/stores/memory-store.js.map +1 -0
  59. package/dist/stores/rate-limit-store.d.ts +6 -0
  60. package/dist/stores/rate-limit-store.d.ts.map +1 -0
  61. package/dist/stores/rate-limit-store.js +2 -0
  62. package/dist/stores/rate-limit-store.js.map +1 -0
  63. package/dist/stores/redis-store.d.ts +26 -0
  64. package/dist/stores/redis-store.d.ts.map +1 -0
  65. package/dist/stores/redis-store.js +41 -0
  66. package/dist/stores/redis-store.js.map +1 -0
  67. package/dist/strategies/sliding-window-strategy.d.ts +31 -0
  68. package/dist/strategies/sliding-window-strategy.d.ts.map +1 -0
  69. package/dist/strategies/sliding-window-strategy.js +212 -0
  70. package/dist/strategies/sliding-window-strategy.js.map +1 -0
  71. package/dist/strategies/token-bucket-strategy.d.ts +30 -0
  72. package/dist/strategies/token-bucket-strategy.d.ts.map +1 -0
  73. package/dist/strategies/token-bucket-strategy.js +154 -0
  74. package/dist/strategies/token-bucket-strategy.js.map +1 -0
  75. package/dist/types.d.ts +48 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +2 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/utils/headers.d.ts +4 -0
  80. package/dist/utils/headers.d.ts.map +1 -0
  81. package/dist/utils/headers.js +16 -0
  82. package/dist/utils/headers.js.map +1 -0
  83. package/dist/utils/http.d.ts +11 -0
  84. package/dist/utils/http.d.ts.map +1 -0
  85. package/dist/utils/http.js +41 -0
  86. package/dist/utils/http.js.map +1 -0
  87. package/dist/utils/math.d.ts +4 -0
  88. package/dist/utils/math.d.ts.map +1 -0
  89. package/dist/utils/math.js +21 -0
  90. package/dist/utils/math.js.map +1 -0
  91. package/package.json +94 -0
@@ -0,0 +1,24 @@
1
+ export class MemoryStore {
2
+ entries = new Map();
3
+ async consume(key, strategy, context) {
4
+ this.pruneExpired(key, context.now);
5
+ const current = this.entries.get(key);
6
+ const snapshot = strategy.evaluate(current?.state, context);
7
+ const expiresAt = context.now + snapshot.ttlMs;
8
+ this.entries.set(key, {
9
+ expiresAt,
10
+ state: snapshot.state,
11
+ });
12
+ return snapshot;
13
+ }
14
+ async reset(key) {
15
+ this.entries.delete(key);
16
+ }
17
+ pruneExpired(key, now) {
18
+ const current = this.entries.get(key);
19
+ if (current && current.expiresAt <= now) {
20
+ this.entries.delete(key);
21
+ }
22
+ }
23
+ }
24
+ //# sourceMappingURL=memory-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-store.js","sourceRoot":"","sources":["../../src/stores/memory-store.ts"],"names":[],"mappings":"AAYA,MAAM,OAAO,WAAW;IACL,OAAO,GAAG,IAAI,GAAG,EAAgC,CAAC;IAEnE,KAAK,CAAC,OAAO,CACX,GAAW,EACX,QAAmC,EACnC,OAAiC;QAEjC,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAEpC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAoC,CAAC;QACzE,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QAC5D,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC;QAE/C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;YACpB,SAAS;YACT,KAAK,EAAE,QAAQ,CAAC,KAAK;SACtB,CAAC,CAAC;QAEH,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAW;QACrB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAEO,YAAY,CAAC,GAAW,EAAE,GAAW;QAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,OAAO,IAAI,OAAO,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;YACxC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import type { RateLimitStrategy, StrategyExecutionContext, StrategyExecutionSnapshot } from "../types.js";
2
+ export interface RateLimitStore {
3
+ consume<TState>(key: string, strategy: RateLimitStrategy<TState>, context: StrategyExecutionContext): Promise<StrategyExecutionSnapshot<TState>>;
4
+ reset(key: string): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=rate-limit-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit-store.d.ts","sourceRoot":"","sources":["../../src/stores/rate-limit-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,iBAAiB,CAAC,MAAM,CAAC,EACnC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,yBAAyB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rate-limit-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit-store.js","sourceRoot":"","sources":["../../src/stores/rate-limit-store.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ import type { RateLimitStrategy, StrategyExecutionContext, StrategyExecutionSnapshot } from "../types.js";
2
+ import type { RateLimitStore } from "./rate-limit-store.js";
3
+ export interface RedisCommandExecutor {
4
+ del(key: string): Promise<void>;
5
+ eval(script: string, key: string, args: string[]): Promise<unknown>;
6
+ }
7
+ export interface NodeRedisLikeClient {
8
+ del(key: string): Promise<unknown>;
9
+ eval(script: string, options: {
10
+ keys: string[];
11
+ arguments: string[];
12
+ }): Promise<unknown>;
13
+ }
14
+ export interface IORedisLikeClient {
15
+ del(key: string): Promise<unknown>;
16
+ eval(script: string, numKeys: number, ...args: string[]): Promise<unknown>;
17
+ }
18
+ export declare class RedisStore implements RateLimitStore {
19
+ private readonly executor;
20
+ constructor(executor: RedisCommandExecutor);
21
+ consume<TState>(key: string, strategy: RateLimitStrategy<TState>, context: StrategyExecutionContext): Promise<StrategyExecutionSnapshot<TState>>;
22
+ reset(key: string): Promise<void>;
23
+ }
24
+ export declare function createNodeRedisExecutor(client: NodeRedisLikeClient): RedisCommandExecutor;
25
+ export declare function createIORedisExecutor(client: IORedisLikeClient): RedisCommandExecutor;
26
+ //# sourceMappingURL=redis-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-store.d.ts","sourceRoot":"","sources":["../../src/stores/redis-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,WAAW,oBAAoB;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CACF,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,GAC/C,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5E;AAED,qBAAa,UAAW,YAAW,cAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,oBAAoB;IAErD,OAAO,CAAC,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,iBAAiB,CAAC,MAAM,CAAC,EACnC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAgBvC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,mBAAmB,GAC1B,oBAAoB,CAYtB;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CAStB"}
@@ -0,0 +1,41 @@
1
+ import { UnsupportedStoreError } from "../errors.js";
2
+ export class RedisStore {
3
+ executor;
4
+ constructor(executor) {
5
+ this.executor = executor;
6
+ }
7
+ async consume(key, strategy, context) {
8
+ if (!strategy.redis) {
9
+ throw new UnsupportedStoreError(`Strategy "${strategy.kind}" does not provide a Redis program.`);
10
+ }
11
+ const raw = await this.executor.eval(strategy.redis.script, key, strategy.redis.getArgs(context));
12
+ return strategy.redis.parse(raw);
13
+ }
14
+ async reset(key) {
15
+ await this.executor.del(key);
16
+ }
17
+ }
18
+ export function createNodeRedisExecutor(client) {
19
+ return {
20
+ async del(key) {
21
+ await client.del(key);
22
+ },
23
+ async eval(script, key, args) {
24
+ return client.eval(script, {
25
+ keys: [key],
26
+ arguments: args,
27
+ });
28
+ },
29
+ };
30
+ }
31
+ export function createIORedisExecutor(client) {
32
+ return {
33
+ async del(key) {
34
+ await client.del(key);
35
+ },
36
+ async eval(script, key, args) {
37
+ return client.eval(script, 1, key, ...args);
38
+ },
39
+ };
40
+ }
41
+ //# sourceMappingURL=redis-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-store.js","sourceRoot":"","sources":["../../src/stores/redis-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AA0BrD,MAAM,OAAO,UAAU;IACQ;IAA7B,YAA6B,QAA8B;QAA9B,aAAQ,GAAR,QAAQ,CAAsB;IAAG,CAAC;IAE/D,KAAK,CAAC,OAAO,CACX,GAAW,EACX,QAAmC,EACnC,OAAiC;QAEjC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpB,MAAM,IAAI,qBAAqB,CAC7B,aAAa,QAAQ,CAAC,IAAI,qCAAqC,CAChE,CAAC;QACJ,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAClC,QAAQ,CAAC,KAAK,CAAC,MAAM,EACrB,GAAG,EACH,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAChC,CAAC;QAEF,OAAO,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAW;QACrB,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;CACF;AAED,MAAM,UAAU,uBAAuB,CACrC,MAA2B;IAE3B,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAW;YACnB,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,GAAW,EAAE,IAAc;YACpD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE;gBACzB,IAAI,EAAE,CAAC,GAAG,CAAC;gBACX,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,MAAyB;IAEzB,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAW;YACnB,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,GAAW,EAAE,IAAc;YACpD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC9C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { RateLimitStrategy, RedisStrategyProgram, StrategyExecutionContext, StrategyExecutionSnapshot } from "../types.js";
2
+ export interface SlidingWindowConfig {
3
+ limit: number;
4
+ windowMs: number;
5
+ }
6
+ export interface SlidingWindowState {
7
+ currentCount: number;
8
+ currentWindowStart: number;
9
+ previousCount: number;
10
+ previousWindowStart: number;
11
+ }
12
+ /**
13
+ * Pattern: Strategy
14
+ * Problem: Sliding window and token bucket use materially different accounting logic.
15
+ * Solution: Both implement the same strategy contract, so the limiter delegates without branching.
16
+ * Trade-off: One extra layer of indirection; justified because these algorithms diverge quickly.
17
+ */
18
+ export declare class SlidingWindowStrategy implements RateLimitStrategy<SlidingWindowState> {
19
+ readonly kind = "sliding-window";
20
+ readonly limit: number;
21
+ readonly policy: string;
22
+ readonly redis: RedisStrategyProgram<SlidingWindowState>;
23
+ private readonly windowMs;
24
+ constructor(config: SlidingWindowConfig);
25
+ initialState(now: number): SlidingWindowState;
26
+ evaluate(currentState: SlidingWindowState | undefined, context: StrategyExecutionContext): StrategyExecutionSnapshot<SlidingWindowState>;
27
+ private advanceState;
28
+ private calculateRetryAfterMs;
29
+ private getWindowStart;
30
+ }
31
+ //# sourceMappingURL=sliding-window-strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliding-window-strategy.d.ts","sourceRoot":"","sources":["../../src/strategies/sliding-window-strategy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,qBAAa,qBACX,YAAW,iBAAiB,CAAC,kBAAkB,CAAC;IAEhD,QAAQ,CAAC,IAAI,oBAAoB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC,kBAAkB,CAAC,CAAC;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,MAAM,EAAE,mBAAmB;IAmBvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB;IAU7C,QAAQ,CACN,YAAY,EAAE,kBAAkB,GAAG,SAAS,EAC5C,OAAO,EAAE,wBAAwB,GAChC,yBAAyB,CAAC,kBAAkB,CAAC;IAiChD,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,qBAAqB;IAoB7B,OAAO,CAAC,cAAc;CAGvB"}
@@ -0,0 +1,212 @@
1
+ import { RateLimiterConfigurationError } from "../errors.js";
2
+ import { clamp, normalizeRawRedisResult, toWholeNumber } from "../utils/math.js";
3
+ /**
4
+ * Pattern: Strategy
5
+ * Problem: Sliding window and token bucket use materially different accounting logic.
6
+ * Solution: Both implement the same strategy contract, so the limiter delegates without branching.
7
+ * Trade-off: One extra layer of indirection; justified because these algorithms diverge quickly.
8
+ */
9
+ export class SlidingWindowStrategy {
10
+ kind = "sliding-window";
11
+ limit;
12
+ policy;
13
+ redis;
14
+ windowMs;
15
+ constructor(config) {
16
+ if (!Number.isFinite(config.limit) || config.limit <= 0) {
17
+ throw new RateLimiterConfigurationError("SlidingWindowStrategy requires a positive limit.");
18
+ }
19
+ if (!Number.isFinite(config.windowMs) || config.windowMs <= 0) {
20
+ throw new RateLimiterConfigurationError("SlidingWindowStrategy requires a positive windowMs.");
21
+ }
22
+ this.limit = Math.floor(config.limit);
23
+ this.windowMs = Math.floor(config.windowMs);
24
+ this.policy = `${this.limit};w=${Math.ceil(this.windowMs / 1000)}`;
25
+ this.redis = createSlidingWindowRedisProgram(this.limit, this.windowMs);
26
+ }
27
+ initialState(now) {
28
+ const windowStart = this.getWindowStart(now);
29
+ return {
30
+ currentCount: 0,
31
+ currentWindowStart: windowStart,
32
+ previousCount: 0,
33
+ previousWindowStart: windowStart - this.windowMs,
34
+ };
35
+ }
36
+ evaluate(currentState, context) {
37
+ const state = this.advanceState(currentState, context.now);
38
+ const elapsed = context.now - state.currentWindowStart;
39
+ const previousWeight = clamp((this.windowMs - elapsed) / this.windowMs, 0, 1);
40
+ const effectiveBefore = state.currentCount + state.previousCount * previousWeight;
41
+ const allowed = effectiveBefore + context.cost <= this.limit + 1e-9;
42
+ if (allowed) {
43
+ state.currentCount += context.cost;
44
+ }
45
+ const effectiveAfter = state.currentCount + state.previousCount * previousWeight;
46
+ const resetAt = state.currentWindowStart + this.windowMs;
47
+ return {
48
+ state,
49
+ allowed,
50
+ used: this.limit - toWholeNumber(this.limit - effectiveAfter),
51
+ remaining: toWholeNumber(this.limit - effectiveAfter),
52
+ resetAt,
53
+ retryAfterMs: allowed
54
+ ? 0
55
+ : this.calculateRetryAfterMs(state, context.cost, elapsed),
56
+ ttlMs: this.windowMs * 2,
57
+ };
58
+ }
59
+ advanceState(currentState, now) {
60
+ if (!currentState) {
61
+ return this.initialState(now);
62
+ }
63
+ const nextWindowStart = this.getWindowStart(now);
64
+ if (currentState.currentWindowStart === nextWindowStart) {
65
+ return { ...currentState };
66
+ }
67
+ if (currentState.currentWindowStart === nextWindowStart - this.windowMs) {
68
+ return {
69
+ currentCount: 0,
70
+ currentWindowStart: nextWindowStart,
71
+ previousCount: currentState.currentCount,
72
+ previousWindowStart: currentState.currentWindowStart,
73
+ };
74
+ }
75
+ return this.initialState(now);
76
+ }
77
+ calculateRetryAfterMs(state, cost, elapsed) {
78
+ const resetAt = state.currentWindowStart + this.windowMs;
79
+ if (state.currentCount + cost > this.limit || state.previousCount <= 0) {
80
+ return Math.max(1, resetAt - (state.currentWindowStart + elapsed));
81
+ }
82
+ const decayBudget = this.limit - cost - state.currentCount;
83
+ if (decayBudget < 0) {
84
+ return Math.max(1, resetAt - (state.currentWindowStart + elapsed));
85
+ }
86
+ const targetWeight = clamp(decayBudget / state.previousCount, 0, 1);
87
+ const targetElapsed = this.windowMs - targetWeight * this.windowMs;
88
+ return Math.max(1, Math.ceil(targetElapsed - elapsed));
89
+ }
90
+ getWindowStart(now) {
91
+ return Math.floor(now / this.windowMs) * this.windowMs;
92
+ }
93
+ }
94
+ function createSlidingWindowRedisProgram(limit, windowMs) {
95
+ const script = `
96
+ local key = KEYS[1]
97
+ local now = tonumber(ARGV[1])
98
+ local cost = tonumber(ARGV[2])
99
+ local limit = tonumber(ARGV[3])
100
+ local windowMs = tonumber(ARGV[4])
101
+
102
+ local raw = redis.call("GET", key)
103
+ local state
104
+ if raw then
105
+ state = cjson.decode(raw)
106
+ else
107
+ local windowStart = math.floor(now / windowMs) * windowMs
108
+ state = {
109
+ currentCount = 0,
110
+ currentWindowStart = windowStart,
111
+ previousCount = 0,
112
+ previousWindowStart = windowStart - windowMs
113
+ }
114
+ end
115
+
116
+ local nextWindowStart = math.floor(now / windowMs) * windowMs
117
+ if state.currentWindowStart ~= nextWindowStart then
118
+ if state.currentWindowStart == nextWindowStart - windowMs then
119
+ state = {
120
+ currentCount = 0,
121
+ currentWindowStart = nextWindowStart,
122
+ previousCount = state.currentCount,
123
+ previousWindowStart = state.currentWindowStart
124
+ }
125
+ else
126
+ state = {
127
+ currentCount = 0,
128
+ currentWindowStart = nextWindowStart,
129
+ previousCount = 0,
130
+ previousWindowStart = nextWindowStart - windowMs
131
+ }
132
+ end
133
+ end
134
+
135
+ local elapsed = now - state.currentWindowStart
136
+ local previousWeight = (windowMs - elapsed) / windowMs
137
+ if previousWeight < 0 then
138
+ previousWeight = 0
139
+ elseif previousWeight > 1 then
140
+ previousWeight = 1
141
+ end
142
+
143
+ local effectiveBefore = state.currentCount + (state.previousCount * previousWeight)
144
+ local allowed = 0
145
+ if effectiveBefore + cost <= limit + 0.000000001 then
146
+ allowed = 1
147
+ state.currentCount = state.currentCount + cost
148
+ end
149
+
150
+ local effectiveAfter = state.currentCount + (state.previousCount * previousWeight)
151
+ local remaining = math.max(0, math.floor(limit - effectiveAfter + 0.000000001))
152
+ local used = limit - remaining
153
+ local resetAt = state.currentWindowStart + windowMs
154
+ local retryAfterMs = 0
155
+
156
+ if allowed == 0 then
157
+ if state.currentCount + cost > limit or state.previousCount <= 0 then
158
+ retryAfterMs = math.max(1, resetAt - now)
159
+ else
160
+ local decayBudget = limit - cost - state.currentCount
161
+ if decayBudget < 0 then
162
+ retryAfterMs = math.max(1, resetAt - now)
163
+ else
164
+ local targetWeight = decayBudget / state.previousCount
165
+ if targetWeight < 0 then
166
+ targetWeight = 0
167
+ elseif targetWeight > 1 then
168
+ targetWeight = 1
169
+ end
170
+ local targetElapsed = windowMs - (targetWeight * windowMs)
171
+ retryAfterMs = math.max(1, math.ceil(targetElapsed - elapsed))
172
+ end
173
+ end
174
+ end
175
+
176
+ local ttlMs = windowMs * 2
177
+ redis.call("PSETEX", key, ttlMs, cjson.encode(state))
178
+ return {
179
+ cjson.encode(state),
180
+ tostring(allowed),
181
+ tostring(used),
182
+ tostring(remaining),
183
+ tostring(resetAt),
184
+ tostring(retryAfterMs),
185
+ tostring(ttlMs)
186
+ }
187
+ `;
188
+ return {
189
+ script,
190
+ getArgs(context) {
191
+ return [
192
+ String(context.now),
193
+ String(context.cost),
194
+ String(limit),
195
+ String(windowMs),
196
+ ];
197
+ },
198
+ parse(raw) {
199
+ const values = normalizeRawRedisResult(raw, "sliding-window");
200
+ return {
201
+ state: JSON.parse(values[0]),
202
+ allowed: values[1] === "1",
203
+ used: Number(values[2]),
204
+ remaining: Number(values[3]),
205
+ resetAt: Number(values[4]),
206
+ retryAfterMs: Number(values[5]),
207
+ ttlMs: Number(values[6]),
208
+ };
209
+ },
210
+ };
211
+ }
212
+ //# sourceMappingURL=sliding-window-strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliding-window-strategy.js","sourceRoot":"","sources":["../../src/strategies/sliding-window-strategy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { RateLimitStrategy, RedisStrategyProgram, StrategyExecutionContext, StrategyExecutionSnapshot } from "../types.js";
2
+ export interface TokenBucketConfig {
3
+ capacity: number;
4
+ refillIntervalMs: number;
5
+ refillRate: number;
6
+ }
7
+ export interface TokenBucketState {
8
+ lastRefillAt: number;
9
+ tokens: number;
10
+ }
11
+ /**
12
+ * Pattern: Strategy
13
+ * Problem: Burst control and rolling windows are separate rate-limit behaviors.
14
+ * Solution: Token bucket is isolated behind the shared strategy contract.
15
+ * Trade-off: Slightly more structure; justified because callers should not care which algorithm is active.
16
+ */
17
+ export declare class TokenBucketStrategy implements RateLimitStrategy<TokenBucketState> {
18
+ readonly kind = "token-bucket";
19
+ readonly limit: number;
20
+ readonly policy: string;
21
+ readonly redis: RedisStrategyProgram<TokenBucketState>;
22
+ private readonly capacity;
23
+ private readonly refillIntervalMs;
24
+ private readonly refillRate;
25
+ private readonly refillPerMs;
26
+ constructor(config: TokenBucketConfig);
27
+ initialState(now: number): TokenBucketState;
28
+ evaluate(currentState: TokenBucketState | undefined, context: StrategyExecutionContext): StrategyExecutionSnapshot<TokenBucketState>;
29
+ }
30
+ //# sourceMappingURL=token-bucket-strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-bucket-strategy.d.ts","sourceRoot":"","sources":["../../src/strategies/token-bucket-strategy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,qBAAa,mBACX,YAAW,iBAAiB,CAAC,gBAAgB,CAAC;IAE9C,QAAQ,CAAC,IAAI,kBAAkB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,MAAM,EAAE,iBAAiB;IAqCrC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB;IAO3C,QAAQ,CACN,YAAY,EAAE,gBAAgB,GAAG,SAAS,EAC1C,OAAO,EAAE,wBAAwB,GAChC,yBAAyB,CAAC,gBAAgB,CAAC;CAiC/C"}
@@ -0,0 +1,154 @@
1
+ import { RateLimiterConfigurationError } from "../errors.js";
2
+ import { clamp, normalizeRawRedisResult, toWholeNumber } from "../utils/math.js";
3
+ /**
4
+ * Pattern: Strategy
5
+ * Problem: Burst control and rolling windows are separate rate-limit behaviors.
6
+ * Solution: Token bucket is isolated behind the shared strategy contract.
7
+ * Trade-off: Slightly more structure; justified because callers should not care which algorithm is active.
8
+ */
9
+ export class TokenBucketStrategy {
10
+ kind = "token-bucket";
11
+ limit;
12
+ policy;
13
+ redis;
14
+ capacity;
15
+ refillIntervalMs;
16
+ refillRate;
17
+ refillPerMs;
18
+ constructor(config) {
19
+ if (!Number.isFinite(config.capacity) || config.capacity <= 0) {
20
+ throw new RateLimiterConfigurationError("TokenBucketStrategy requires a positive capacity.");
21
+ }
22
+ if (!Number.isFinite(config.refillIntervalMs) ||
23
+ config.refillIntervalMs <= 0) {
24
+ throw new RateLimiterConfigurationError("TokenBucketStrategy requires a positive refillIntervalMs.");
25
+ }
26
+ if (!Number.isFinite(config.refillRate) || config.refillRate <= 0) {
27
+ throw new RateLimiterConfigurationError("TokenBucketStrategy requires a positive refillRate.");
28
+ }
29
+ this.capacity = config.capacity;
30
+ this.refillIntervalMs = config.refillIntervalMs;
31
+ this.refillRate = config.refillRate;
32
+ this.refillPerMs = this.refillRate / this.refillIntervalMs;
33
+ this.limit = Math.floor(this.capacity);
34
+ this.policy = `${this.limit};w=${Math.ceil(this.refillIntervalMs / 1000)};burst=${this.limit}`;
35
+ this.redis = createTokenBucketRedisProgram(this.capacity, this.refillRate, this.refillIntervalMs);
36
+ }
37
+ initialState(now) {
38
+ return {
39
+ lastRefillAt: now,
40
+ tokens: this.capacity,
41
+ };
42
+ }
43
+ evaluate(currentState, context) {
44
+ const state = currentState
45
+ ? { ...currentState }
46
+ : this.initialState(context.now);
47
+ if (context.now > state.lastRefillAt) {
48
+ const refill = (context.now - state.lastRefillAt) * this.refillPerMs;
49
+ state.tokens = clamp(state.tokens + refill, 0, this.capacity);
50
+ state.lastRefillAt = context.now;
51
+ }
52
+ const allowed = state.tokens + 1e-9 >= context.cost;
53
+ if (allowed) {
54
+ state.tokens = clamp(state.tokens - context.cost, 0, this.capacity);
55
+ }
56
+ const retryAfterMs = allowed
57
+ ? 0
58
+ : Math.max(1, Math.ceil((context.cost - state.tokens) / this.refillPerMs));
59
+ const timeToFullMs = Math.ceil((this.capacity - state.tokens) / this.refillPerMs);
60
+ return {
61
+ state,
62
+ allowed,
63
+ used: this.limit - toWholeNumber(state.tokens),
64
+ remaining: toWholeNumber(state.tokens),
65
+ resetAt: context.now + Math.max(0, timeToFullMs),
66
+ retryAfterMs,
67
+ ttlMs: Math.max(this.refillIntervalMs, timeToFullMs),
68
+ };
69
+ }
70
+ }
71
+ function createTokenBucketRedisProgram(capacity, refillRate, refillIntervalMs) {
72
+ const script = `
73
+ local key = KEYS[1]
74
+ local now = tonumber(ARGV[1])
75
+ local cost = tonumber(ARGV[2])
76
+ local capacity = tonumber(ARGV[3])
77
+ local refillRate = tonumber(ARGV[4])
78
+ local refillIntervalMs = tonumber(ARGV[5])
79
+ local refillPerMs = refillRate / refillIntervalMs
80
+
81
+ local raw = redis.call("GET", key)
82
+ local state
83
+ if raw then
84
+ state = cjson.decode(raw)
85
+ else
86
+ state = {
87
+ lastRefillAt = now,
88
+ tokens = capacity
89
+ }
90
+ end
91
+
92
+ if now > state.lastRefillAt then
93
+ local refill = (now - state.lastRefillAt) * refillPerMs
94
+ state.tokens = math.min(capacity, math.max(0, state.tokens + refill))
95
+ state.lastRefillAt = now
96
+ end
97
+
98
+ local allowed = 0
99
+ if state.tokens + 0.000000001 >= cost then
100
+ allowed = 1
101
+ state.tokens = math.min(capacity, math.max(0, state.tokens - cost))
102
+ end
103
+
104
+ local remaining = math.max(0, math.floor(state.tokens + 0.000000001))
105
+ local used = math.floor(capacity) - remaining
106
+ local retryAfterMs = 0
107
+ if allowed == 0 then
108
+ retryAfterMs = math.max(1, math.ceil((cost - state.tokens) / refillPerMs))
109
+ end
110
+
111
+ local timeToFullMs = math.ceil((capacity - state.tokens) / refillPerMs)
112
+ if timeToFullMs < 0 then
113
+ timeToFullMs = 0
114
+ end
115
+ local ttlMs = math.max(refillIntervalMs, timeToFullMs)
116
+ local resetAt = now + timeToFullMs
117
+
118
+ redis.call("PSETEX", key, ttlMs, cjson.encode(state))
119
+ return {
120
+ cjson.encode(state),
121
+ tostring(allowed),
122
+ tostring(used),
123
+ tostring(remaining),
124
+ tostring(resetAt),
125
+ tostring(retryAfterMs),
126
+ tostring(ttlMs)
127
+ }
128
+ `;
129
+ return {
130
+ script,
131
+ getArgs(context) {
132
+ return [
133
+ String(context.now),
134
+ String(context.cost),
135
+ String(capacity),
136
+ String(refillRate),
137
+ String(refillIntervalMs),
138
+ ];
139
+ },
140
+ parse(raw) {
141
+ const values = normalizeRawRedisResult(raw, "token-bucket");
142
+ return {
143
+ state: JSON.parse(values[0]),
144
+ allowed: values[1] === "1",
145
+ used: Number(values[2]),
146
+ remaining: Number(values[3]),
147
+ resetAt: Number(values[4]),
148
+ retryAfterMs: Number(values[5]),
149
+ ttlMs: Number(values[6]),
150
+ };
151
+ },
152
+ };
153
+ }
154
+ //# sourceMappingURL=token-bucket-strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-bucket-strategy.js","sourceRoot":"","sources":["../../src/strategies/token-bucket-strategy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ export interface RateLimitCheckOptions {
2
+ cost?: number;
3
+ }
4
+ export interface RateLimitDecision {
5
+ key: string;
6
+ allowed: boolean;
7
+ strategy: string;
8
+ policy: string;
9
+ limit: number;
10
+ used: number;
11
+ remaining: number;
12
+ checkedAt: Date;
13
+ resetAt: Date;
14
+ resetAfterMs: number;
15
+ retryAfterMs: number;
16
+ retryAfterSeconds: number;
17
+ }
18
+ export interface RateLimiterLike {
19
+ check(key: string, options?: RateLimitCheckOptions): Promise<RateLimitDecision>;
20
+ reset(key: string): Promise<void>;
21
+ }
22
+ export interface StrategyExecutionContext {
23
+ now: number;
24
+ cost: number;
25
+ }
26
+ export interface StrategyExecutionSnapshot<TState> {
27
+ state: TState;
28
+ allowed: boolean;
29
+ used: number;
30
+ remaining: number;
31
+ resetAt: number;
32
+ retryAfterMs: number;
33
+ ttlMs: number;
34
+ }
35
+ export interface RedisStrategyProgram<TState> {
36
+ script: string;
37
+ getArgs(context: StrategyExecutionContext): string[];
38
+ parse(raw: unknown): StrategyExecutionSnapshot<TState>;
39
+ }
40
+ export interface RateLimitStrategy<TState> {
41
+ kind: string;
42
+ limit: number;
43
+ policy: string;
44
+ initialState(now: number): TState;
45
+ evaluate(currentState: TState | undefined, context: StrategyExecutionContext): StrategyExecutionSnapshot<TState>;
46
+ redis?: RedisStrategyProgram<TState>;
47
+ }
48
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAChF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,yBAAyB,CAAC,MAAM;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB,CAAC,MAAM;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,EAAE,CAAC;IACrD,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,iBAAiB,CAAC,MAAM;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAClC,QAAQ,CACN,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,OAAO,EAAE,wBAAwB,GAChC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC;CACtC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ import type { RateLimitDecision } from "../types.js";
2
+ export declare function buildRateLimitHeaders(decision: RateLimitDecision): Record<string, string>;
3
+ export declare function applyHeaders(setHeader: (name: string, value: string) => void, decision: RateLimitDecision): void;
4
+ //# sourceMappingURL=headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/utils/headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,iBAAiB,GAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,EAChD,QAAQ,EAAE,iBAAiB,GAC1B,IAAI,CAKN"}