@rupayan-das/request-guard 1.0.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/config/redis.config.d.ts +3 -0
  4. package/dist/config/redis.config.d.ts.map +1 -0
  5. package/dist/config/redis.config.js +6 -0
  6. package/dist/config/redis.config.js.map +1 -0
  7. package/dist/index.d.ts +10 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +6 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/interfaces/embedding.interface.d.ts +4 -0
  12. package/dist/interfaces/embedding.interface.d.ts.map +1 -0
  13. package/dist/interfaces/embedding.interface.js +2 -0
  14. package/dist/interfaces/embedding.interface.js.map +1 -0
  15. package/dist/interfaces/llm.interface.d.ts +4 -0
  16. package/dist/interfaces/llm.interface.d.ts.map +1 -0
  17. package/dist/interfaces/llm.interface.js +2 -0
  18. package/dist/interfaces/llm.interface.js.map +1 -0
  19. package/dist/rate_limiter/algorithms/leakyBucket.algo.d.ts +37 -0
  20. package/dist/rate_limiter/algorithms/leakyBucket.algo.d.ts.map +1 -0
  21. package/dist/rate_limiter/algorithms/leakyBucket.algo.js +73 -0
  22. package/dist/rate_limiter/algorithms/leakyBucket.algo.js.map +1 -0
  23. package/dist/rate_limiter/algorithms/slidingWindow.algo.d.ts +13 -0
  24. package/dist/rate_limiter/algorithms/slidingWindow.algo.d.ts.map +1 -0
  25. package/dist/rate_limiter/algorithms/slidingWindow.algo.js +56 -0
  26. package/dist/rate_limiter/algorithms/slidingWindow.algo.js.map +1 -0
  27. package/dist/rate_limiter/algorithms/tokenBucket.algo.d.ts +37 -0
  28. package/dist/rate_limiter/algorithms/tokenBucket.algo.d.ts.map +1 -0
  29. package/dist/rate_limiter/algorithms/tokenBucket.algo.js +88 -0
  30. package/dist/rate_limiter/algorithms/tokenBucket.algo.js.map +1 -0
  31. package/dist/rate_limiter/rateLimiter.factory.d.ts +22 -0
  32. package/dist/rate_limiter/rateLimiter.factory.d.ts.map +1 -0
  33. package/dist/rate_limiter/rateLimiter.factory.js +15 -0
  34. package/dist/rate_limiter/rateLimiter.factory.js.map +1 -0
  35. package/dist/rate_limiter/rateLimiter.interface.d.ts +5 -0
  36. package/dist/rate_limiter/rateLimiter.interface.d.ts.map +1 -0
  37. package/dist/rate_limiter/rateLimiter.interface.js +2 -0
  38. package/dist/rate_limiter/rateLimiter.interface.js.map +1 -0
  39. package/dist/semantic_cache/config/semantic_cache.config.d.ts +2 -0
  40. package/dist/semantic_cache/config/semantic_cache.config.d.ts.map +1 -0
  41. package/dist/semantic_cache/config/semantic_cache.config.js +2 -0
  42. package/dist/semantic_cache/config/semantic_cache.config.js.map +1 -0
  43. package/dist/semantic_cache/services/semantic_cache.service.d.ts +23 -0
  44. package/dist/semantic_cache/services/semantic_cache.service.d.ts.map +1 -0
  45. package/dist/semantic_cache/services/semantic_cache.service.js +82 -0
  46. package/dist/semantic_cache/services/semantic_cache.service.js.map +1 -0
  47. package/dist/semantic_cache/utils/similarity.utils.d.ts +2 -0
  48. package/dist/semantic_cache/utils/similarity.utils.d.ts.map +1 -0
  49. package/dist/semantic_cache/utils/similarity.utils.js +18 -0
  50. package/dist/semantic_cache/utils/similarity.utils.js.map +1 -0
  51. package/dist/semantic_cache/utils/vector.utils.d.ts +2 -0
  52. package/dist/semantic_cache/utils/vector.utils.d.ts.map +1 -0
  53. package/dist/semantic_cache/utils/vector.utils.js +5 -0
  54. package/dist/semantic_cache/utils/vector.utils.js.map +1 -0
  55. package/package.json +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rupayan Das
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # Request Guard
2
+
3
+ Redis-based rate limiting and semantic caching for Node.js applications.
4
+
5
+ ## Features
6
+
7
+ * Token Bucket rate limiting
8
+ * Leaky Bucket rate limiting
9
+ * Sliding Window rate limiting
10
+ * Redis-backed state
11
+ * Semantic caching for LLM responses
12
+ * Pluggable LLM and embedding providers
13
+ * TypeScript support
14
+ * ESM support
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @rupayan-das/request-guard redis
20
+ ```
21
+
22
+ Make sure Redis is running and accessible from your application.
23
+
24
+ Semantic caching requires Redis with vector search support.
25
+
26
+ ## Rate Limiting
27
+
28
+ ### Using the Factory
29
+
30
+ ```ts
31
+ import { createRateLimiter } from "@rupayan-das/request-guard";
32
+ import { createClient } from "redis";
33
+
34
+ const client = createClient({
35
+ url: "redis://localhost:6379",
36
+ });
37
+
38
+ await client.connect();
39
+
40
+ const rateLimiter = createRateLimiter({
41
+ algorithm: "token-bucket",
42
+ capacity: 10,
43
+ refillRate: 1,
44
+ client,
45
+ });
46
+
47
+ const allowed = await rateLimiter.allowRequest("user-123");
48
+
49
+ console.log(allowed);
50
+ ```
51
+
52
+ `allowRequest()` returns `true` when the request is allowed and `false` when the rate limit has been exceeded.
53
+
54
+ ## Available Algorithms
55
+
56
+ ### Token Bucket
57
+
58
+ ```ts
59
+ const rateLimiter = createRateLimiter({
60
+ algorithm: "token-bucket",
61
+ capacity: 10,
62
+ refillRate: 1,
63
+ client,
64
+ });
65
+ ```
66
+
67
+ * `capacity`: Maximum number of tokens in the bucket.
68
+ * `refillRate`: Number of tokens added per second.
69
+
70
+ ### Leaky Bucket
71
+
72
+ ```ts
73
+ const rateLimiter = createRateLimiter({
74
+ algorithm: "leaky-bucket",
75
+ capacity: 10,
76
+ leakRate: 1,
77
+ client,
78
+ });
79
+ ```
80
+
81
+ * `capacity`: Maximum bucket capacity.
82
+ * `leakRate`: Number of requests processed per second.
83
+
84
+ ### Sliding Window
85
+
86
+ ```ts
87
+ const rateLimiter = createRateLimiter({
88
+ algorithm: "sliding-window",
89
+ windowSize: 60,
90
+ maxRequests: 10,
91
+ client,
92
+ });
93
+ ```
94
+
95
+ * `windowSize`: Window duration in seconds.
96
+ * `maxRequests`: Maximum number of requests allowed within the window.
97
+
98
+ ## Semantic Cache
99
+
100
+ Semantic caching uses embeddings to determine whether a new query is sufficiently similar to a previously cached query.
101
+
102
+ ```ts
103
+ import { SemanticCache } from "@rupayan-das/request-guard";
104
+
105
+ const cache = new SemanticCache({
106
+ client,
107
+ embeddingProvider,
108
+ llmProvider,
109
+ threshold: 0.6,
110
+ });
111
+
112
+ const response = await cache.getOrGenerate(
113
+ "Explain how Redis works"
114
+ );
115
+
116
+ console.log(response);
117
+ ```
118
+
119
+ If a sufficiently similar query exists in the cache, its cached response is returned instead of generating a new LLM response.
120
+
121
+ The default similarity threshold is `0.6`.
122
+
123
+ A higher threshold requires queries to be more similar to produce a cache hit. A lower threshold allows more loosely related queries to use cached responses.
124
+
125
+ ## Providers
126
+
127
+ The semantic cache does not depend on a specific AI provider.
128
+
129
+ You provide implementations of the `EmbeddingProvider` and `LLMProvider` interfaces.
130
+
131
+ ### Embedding Provider
132
+
133
+ ```ts
134
+ interface EmbeddingProvider {
135
+ generateEmbedding(text: string): Promise<number[]>;
136
+ }
137
+ ```
138
+
139
+ ### LLM Provider
140
+
141
+ ```ts
142
+ interface LLMProvider {
143
+ askLLM(prompt: string): Promise<string>;
144
+ }
145
+ ```
146
+
147
+ This allows the semantic cache to work with different LLM and embedding providers.
148
+
149
+ For example, you can implement these interfaces using OpenAI, Anthropic, Gemini, a local model, or another provider.
150
+
151
+ ## Direct Algorithm Usage
152
+
153
+ The individual rate limiting algorithms can also be instantiated directly.
154
+
155
+ ```ts
156
+ import { TokenBucket } from "@rupayan-das/request-guard";
157
+
158
+ const limiter = new TokenBucket(
159
+ 10,
160
+ 1,
161
+ client
162
+ );
163
+
164
+ const allowed = await limiter.allowRequest("user-123");
165
+
166
+ console.log(allowed);
167
+ ```
168
+
169
+ The same approach can be used with `LeakyBucket` and `SlidingWindow`.
170
+
171
+ ## API
172
+
173
+ ### `createRateLimiter(options)`
174
+
175
+ Creates a rate limiter using one of the supported algorithms:
176
+
177
+ * `token-bucket`
178
+ * `leaky-bucket`
179
+ * `sliding-window`
180
+
181
+ ### `SemanticCache(options)`
182
+
183
+ Creates a semantic cache using:
184
+
185
+ * A Redis client
186
+ * An embedding provider
187
+ * An LLM provider
188
+ * An optional similarity threshold
189
+
190
+ ### `allowRequest(userId)`
191
+
192
+ Checks whether a request should be allowed for the specified user.
193
+
194
+ Returns:
195
+
196
+ ```ts
197
+ Promise<boolean>
198
+ ```
199
+
200
+ * `true`: Request is allowed.
201
+ * `false`: Rate limit has been exceeded.
202
+
203
+ ### `getOrGenerate(query)`
204
+
205
+ Checks the semantic cache for a sufficiently similar query.
206
+
207
+ * Returns the cached response on a cache hit.
208
+ * Calls the configured LLM provider on a cache miss.
209
+ * Stores the generated response in the semantic cache.
210
+
211
+ Returns:
212
+
213
+ ```ts
214
+ Promise<string>
215
+ ```
216
+
217
+ ## Redis
218
+
219
+ The rate limiter uses Redis to store rate limiting state.
220
+
221
+ The semantic cache uses Redis to store cached queries, responses, and embeddings and to perform vector similarity searches.
222
+
223
+ Make sure your Redis instance is accessible from your Node.js application.
224
+
225
+ ## Requirements
226
+
227
+ * Node.js `>= 20`
228
+ * Redis with vector search support for semantic caching
229
+
230
+ ## License
231
+
232
+ ISC
@@ -0,0 +1,3 @@
1
+ declare const client: import("redis").RedisClientType<{}, {}, {}, 3, {}>;
2
+ export { client, };
3
+ //# sourceMappingURL=redis.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.config.d.ts","sourceRoot":"","sources":["../../src/config/redis.config.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,MAAM,oDAAiB,CAAC;AAM9B,OAAO,EACH,MAAM,GACT,CAAA"}
@@ -0,0 +1,6 @@
1
+ import { createClient } from "redis";
2
+ const client = createClient();
3
+ client.on('error', err => console.log('Redis Client Error', err));
4
+ await client.connect();
5
+ export { client, };
6
+ //# sourceMappingURL=redis.config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.config.js","sourceRoot":"","sources":["../../src/config/redis.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAErC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;AAE9B,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC,CAAC;AAElE,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;AAEvB,OAAO,EACH,MAAM,GACT,CAAA"}
@@ -0,0 +1,10 @@
1
+ export { TokenBucket } from "./rate_limiter/algorithms/tokenBucket.algo.js";
2
+ export { LeakyBucket } from "./rate_limiter/algorithms/leakyBucket.algo.js";
3
+ export { SlidingWindow } from "./rate_limiter/algorithms/slidingWindow.algo.js";
4
+ export { createRateLimiter } from "./rate_limiter/rateLimiter.factory.js";
5
+ export { SemanticCache } from "./semantic_cache/services/semantic_cache.service.js";
6
+ export type { RateLimiter } from "./rate_limiter/rateLimiter.interface.js";
7
+ export type { RateLimiterOptions } from "./rate_limiter/rateLimiter.factory.js";
8
+ export type { LLMProvider } from "./interfaces/llm.interface.js";
9
+ export type { EmbeddingProvider } from "./interfaces/embedding.interface.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,+CAA+C,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,+CAA+C,CAAC;AAC5E,OAAO,EAAE,aAAa,EAAE,MAAM,iDAAiD,CAAC;AAEhF,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAG1E,OAAO,EAAE,aAAa,EAAE,MAAM,qDAAqD,CAAC;AAEpF,YAAY,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAC3E,YAAY,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAChF,YAAY,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,YAAY,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { TokenBucket } from "./rate_limiter/algorithms/tokenBucket.algo.js";
2
+ export { LeakyBucket } from "./rate_limiter/algorithms/leakyBucket.algo.js";
3
+ export { SlidingWindow } from "./rate_limiter/algorithms/slidingWindow.algo.js";
4
+ export { createRateLimiter } from "./rate_limiter/rateLimiter.factory.js";
5
+ export { SemanticCache } from "./semantic_cache/services/semantic_cache.service.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,+CAA+C,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,+CAA+C,CAAC;AAC5E,OAAO,EAAE,aAAa,EAAE,MAAM,iDAAiD,CAAC;AAEhF,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAG1E,OAAO,EAAE,aAAa,EAAE,MAAM,qDAAqD,CAAC"}
@@ -0,0 +1,4 @@
1
+ export interface EmbeddingProvider {
2
+ generateEmbedding(text: string): Promise<number[]>;
3
+ }
4
+ //# sourceMappingURL=embedding.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/embedding.interface.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAC9B,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACtD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=embedding.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedding.interface.js","sourceRoot":"","sources":["../../src/interfaces/embedding.interface.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ export interface LLMProvider {
2
+ askLLM(query: string): Promise<string>;
3
+ }
4
+ //# sourceMappingURL=llm.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/llm.interface.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IACxB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=llm.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.interface.js","sourceRoot":"","sources":["../../src/interfaces/llm.interface.ts"],"names":[],"mappings":""}
@@ -0,0 +1,37 @@
1
+ import type { RedisClientType } from "redis";
2
+ import type { RateLimiter } from "../rateLimiter.interface.js";
3
+ declare class LeakyBucket implements RateLimiter {
4
+ private capacity;
5
+ private leakRate;
6
+ private client;
7
+ private readonly script;
8
+ constructor(capacity: number, leakRate: number, client: RedisClientType);
9
+ allowRequest(userId: string): Promise<boolean>;
10
+ runScript(userId: string): Promise<string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | /*elided*/ any | import("redis").BlobError | import("redis").SimpleError | {
11
+ [x: string]: string | number | bigint | boolean | /*elided*/ any | import("redis").BlobError | import("redis").SimpleError | /*elided*/ any | null;
12
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
13
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
14
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
15
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
16
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
17
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
18
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
19
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
20
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
21
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
22
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
23
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
24
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
25
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
26
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
27
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
28
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
29
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
30
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
31
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
32
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
33
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
34
+ } | null>;
35
+ }
36
+ export { LeakyBucket };
37
+ //# sourceMappingURL=leakyBucket.algo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leakyBucket.algo.d.ts","sourceRoot":"","sources":["../../../src/rate_limiter/algorithms/leakyBucket.algo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE/D,cAAM,WAAY,YAAW,WAAW;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAkB;IAEhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CA0CrB;IAEF,YAAY,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAItE;IAEK,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnD;IAGK,SAAS,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;cAU7B;CAGJ;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -0,0 +1,73 @@
1
+ class LeakyBucket {
2
+ capacity;
3
+ leakRate;
4
+ client;
5
+ script = `
6
+ local capacity = tonumber(ARGV[1])
7
+ local leakRate = tonumber(ARGV[2])
8
+ local now = tonumber(ARGV[3])
9
+ local lastLeakTime = tonumber(redis.call("HGET", KEYS[1], "lastLeakTime"))
10
+ local currentLevel = tonumber(redis.call("HGET", KEYS[1], "currentLevel"))
11
+
12
+ if currentLevel == nil then
13
+ currentLevel = 0
14
+ lastLeakTime = now
15
+ redis.call("HSET", KEYS[1],
16
+ "currentLevel", currentLevel,
17
+ "lastLeakTime", now
18
+ )
19
+
20
+ else
21
+ lastLeakTime = tonumber(redis.call("HGET", KEYS[1], "lastLeakTime"))
22
+ end
23
+
24
+ local elapsedTime = (now - lastLeakTime) / 1000
25
+ local leakedRequests = math.floor(leakRate * elapsedTime)
26
+
27
+ if leakedRequests > 0 then
28
+ currentLevel = currentLevel - leakedRequests
29
+ if (currentLevel < 0) then
30
+ currentLevel = 0
31
+ end
32
+ lastLeakTime = lastLeakTime + (leakedRequests / leakRate) * 1000
33
+ end
34
+
35
+ local allowed = 0
36
+ if (currentLevel < capacity) then
37
+ currentLevel = currentLevel + 1
38
+ allowed = 1
39
+ end
40
+
41
+ redis.call("HSET", KEYS[1],
42
+ "currentLevel", currentLevel,
43
+ "lastLeakTime", lastLeakTime
44
+ )
45
+
46
+ return allowed
47
+ `;
48
+ constructor(capacity, leakRate, client) {
49
+ this.capacity = capacity;
50
+ this.leakRate = leakRate;
51
+ this.client = client;
52
+ }
53
+ async allowRequest(userId) {
54
+ const result = await this.runScript(userId);
55
+ if (result === 1) {
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ async runScript(userId) {
61
+ const result = await this.client.eval(this.script, {
62
+ keys: [`leaky_bucket:${userId}`],
63
+ arguments: [
64
+ String(this.capacity),
65
+ String(this.leakRate),
66
+ String(Date.now()),
67
+ ],
68
+ });
69
+ return result;
70
+ }
71
+ }
72
+ export { LeakyBucket };
73
+ //# sourceMappingURL=leakyBucket.algo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leakyBucket.algo.js","sourceRoot":"","sources":["../../../src/rate_limiter/algorithms/leakyBucket.algo.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW;IACL,QAAQ,CAAS;IACjB,QAAQ,CAAS;IACjB,MAAM,CAAkB;IAEf,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0CzB,CAAC;IAEF,YAAY,QAAgB,EAAE,QAAgB,EAAE,MAAuB;QACnE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,MAAc;QAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAE5C,IAAG,MAAM,KAAK,CAAC,EAAC,CAAC;YACb,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAGD,KAAK,CAAC,SAAS,CAAC,MAAc;QAC1B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YAC/C,IAAI,EAAE,CAAC,gBAAgB,MAAM,EAAE,CAAC;YAChC,SAAS,EAAE;gBACP,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;gBACrB,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;gBACrB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;aACrB;SACJ,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAClB,CAAC;CAGJ;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { RedisClientType } from "redis";
2
+ import type { RateLimiter } from "../rateLimiter.interface.js";
3
+ declare class SlidingWindow implements RateLimiter {
4
+ private windowSize;
5
+ private maxRequests;
6
+ private client;
7
+ private readonly script;
8
+ constructor(windowSize: number, maxRequests: number, client: RedisClientType);
9
+ allowRequest(userId: string): Promise<boolean>;
10
+ runScript(userId: string, requestId: string): Promise<number>;
11
+ }
12
+ export { SlidingWindow, };
13
+ //# sourceMappingURL=slidingWindow.algo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slidingWindow.algo.d.ts","sourceRoot":"","sources":["../../../src/rate_limiter/algorithms/slidingWindow.algo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE/D,cAAM,aAAc,YAAW,WAAW;IACtC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAkB;IAEhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CA0BtB;IACD,YAAY,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAI3E;IAGK,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAInD;IAGK,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAWlE;CAEJ;AAED,OAAM,EACF,aAAa,GAChB,CAAA"}
@@ -0,0 +1,56 @@
1
+ class SlidingWindow {
2
+ windowSize;
3
+ maxRequests;
4
+ client;
5
+ script = `
6
+ local now = tonumber(ARGV[1])
7
+ local maxRequests = tonumber(ARGV[2])
8
+ local windowSize = tonumber(ARGV[3])
9
+ local requestId = ARGV[4]
10
+ local windowStart = now - windowSize * 1000
11
+
12
+ redis.call(
13
+ "ZREMRANGEBYSCORE",
14
+ KEYS[1],
15
+ 0,
16
+ windowStart
17
+ )
18
+
19
+ local requestCount = redis.call("ZCARD", KEYS[1])
20
+ if requestCount < maxRequests then
21
+ redis.call(
22
+ "ZADD",
23
+ KEYS[1],
24
+ now,
25
+ requestId
26
+ )
27
+ return 1
28
+ end
29
+
30
+ return 0
31
+ `;
32
+ constructor(windowSize, maxRequests, client) {
33
+ this.windowSize = windowSize;
34
+ this.maxRequests = maxRequests;
35
+ this.client = client;
36
+ }
37
+ async allowRequest(userId) {
38
+ const requestId = `${Date.now()}-${crypto.randomUUID()}`;
39
+ const result = await this.runScript(userId, requestId);
40
+ return Boolean(result);
41
+ }
42
+ async runScript(userId, requestId) {
43
+ const result = await this.client.eval(this.script, {
44
+ keys: [`sliding_window:${userId}`],
45
+ arguments: [
46
+ String(Date.now()),
47
+ String(this.maxRequests),
48
+ String(this.windowSize),
49
+ requestId
50
+ ],
51
+ });
52
+ return result;
53
+ }
54
+ }
55
+ export { SlidingWindow, };
56
+ //# sourceMappingURL=slidingWindow.algo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slidingWindow.algo.js","sourceRoot":"","sources":["../../../src/rate_limiter/algorithms/slidingWindow.algo.ts"],"names":[],"mappings":"AAGA,MAAM,aAAa;IACP,UAAU,CAAS;IACnB,WAAW,CAAS;IACpB,MAAM,CAAkB;IAEf,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;KA0BzB,CAAA;IACD,YAAY,UAAkB,EAAE,WAAmB,EAAE,MAAuB;QACxE,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAGD,KAAK,CAAC,YAAY,CAAC,MAAc;QAC7B,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC;QACzD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QACvD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAGD,KAAK,CAAC,SAAS,CAAC,MAAc,EAAE,SAAiB;QAC7C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YAC/C,IAAI,EAAE,CAAC,kBAAkB,MAAM,EAAE,CAAC;YAClC,SAAS,EAAE;gBACP,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;gBAClB,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;gBACxB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;gBACvB,SAAS;aACZ;SACJ,CAAC,CAAC;QACH,OAAO,MAAgB,CAAC;IAC5B,CAAC;CAEJ;AAED,OAAM,EACF,aAAa,GAChB,CAAA"}
@@ -0,0 +1,37 @@
1
+ import type { RedisClientType } from "redis";
2
+ import type { RateLimiter } from "../rateLimiter.interface.js";
3
+ declare class TokenBucket implements RateLimiter {
4
+ private capacity;
5
+ private refillRate;
6
+ private client;
7
+ private readonly script;
8
+ constructor(capacity: number, refillRate: number, client: RedisClientType);
9
+ allowRequest(userId: string): Promise<boolean>;
10
+ runScript(userId: string): Promise<string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | (string | number | bigint | boolean | /*elided*/ any | import("redis").BlobError | import("redis").SimpleError | {
11
+ [x: string]: string | number | bigint | boolean | /*elided*/ any | import("redis").BlobError | import("redis").SimpleError | /*elided*/ any | null;
12
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
13
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
14
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
15
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
16
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
17
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
18
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
19
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
20
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
21
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
22
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
23
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
24
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
25
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
26
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
27
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
28
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
29
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
30
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
31
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
32
+ } | null)[] | import("redis").BlobError | import("redis").SimpleError | {
33
+ [x: string]: string | number | bigint | boolean | any | import("redis").BlobError | import("redis").SimpleError | any | null;
34
+ } | null>;
35
+ }
36
+ export { TokenBucket, };
37
+ //# sourceMappingURL=tokenBucket.algo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenBucket.algo.d.ts","sourceRoot":"","sources":["../../../src/rate_limiter/algorithms/tokenBucket.algo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE/D,cAAM,WAAY,YAAW,WAAW;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAyDrB;IAEF,YAAY,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAIxE;IAEK,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnD;IAEK,SAAS,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;cAU7B;CAGJ;AAGD,OAAO,EACH,WAAW,GACd,CAAA"}
@@ -0,0 +1,88 @@
1
+ class TokenBucket {
2
+ capacity;
3
+ refillRate;
4
+ client;
5
+ script = `
6
+ local now = tonumber(ARGV[3])
7
+ local capacity = tonumber(ARGV[1])
8
+ local refillRate = tonumber(ARGV[2])
9
+
10
+ local tokens = tonumber(
11
+ redis.call("HGET", KEYS[1], "tokens")
12
+ )
13
+
14
+ local lastRefillTime
15
+
16
+ if tokens == nil then
17
+
18
+ tokens = capacity
19
+ lastRefillTime = now
20
+
21
+ redis.call("HSET", KEYS[1],
22
+ "tokens", tokens,
23
+ "lastRefillTime", lastRefillTime
24
+ )
25
+
26
+ else
27
+
28
+ lastRefillTime = tonumber(
29
+ redis.call("HGET", KEYS[1], "lastRefillTime")
30
+ )
31
+
32
+ end
33
+
34
+ local elapsedTime = (now - lastRefillTime) / 1000
35
+
36
+ local tokensToAdd = elapsedTime * refillRate
37
+
38
+ local newTokens = math.min(
39
+ capacity,
40
+ tokens + tokensToAdd
41
+ )
42
+
43
+ if newTokens >= 1 then
44
+
45
+ newTokens = newTokens - 1
46
+
47
+ redis.call("HSET", KEYS[1],
48
+ "tokens", newTokens,
49
+ "lastRefillTime", now
50
+ )
51
+
52
+ return 1
53
+
54
+ end
55
+
56
+ redis.call("HSET", KEYS[1],
57
+ "tokens", newTokens,
58
+ "lastRefillTime", now
59
+ )
60
+
61
+ return 0
62
+ `;
63
+ constructor(capacity, refillRate, client) {
64
+ this.capacity = capacity;
65
+ this.refillRate = refillRate;
66
+ this.client = client;
67
+ }
68
+ async allowRequest(userId) {
69
+ const result = await this.runScript(userId);
70
+ if (result === 1) {
71
+ return true;
72
+ }
73
+ return false;
74
+ }
75
+ async runScript(userId) {
76
+ const result = await this.client.eval(this.script, {
77
+ keys: [`token_bucket:${userId}`],
78
+ arguments: [
79
+ String(this.capacity),
80
+ String(this.refillRate),
81
+ String(Date.now()),
82
+ ],
83
+ });
84
+ return result;
85
+ }
86
+ }
87
+ export { TokenBucket, };
88
+ //# sourceMappingURL=tokenBucket.algo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenBucket.algo.js","sourceRoot":"","sources":["../../../src/rate_limiter/algorithms/tokenBucket.algo.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW;IACL,QAAQ,CAAS;IACjB,UAAU,CAAS;IACnB,MAAM,CAAkB;IACf,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyDzB,CAAC;IAEF,YAAY,QAAgB,EAAE,UAAkB,EAAE,MAAuB;QACrE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,MAAc;QAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAE5C,IAAG,MAAM,KAAK,CAAC,EAAC,CAAC;YACb,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,MAAc;QAC1B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YAC/C,IAAI,EAAE,CAAC,gBAAgB,MAAM,EAAE,CAAC;YAChC,SAAS,EAAE;gBACP,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;gBACrB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;gBACvB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;aACrB;SACJ,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAClB,CAAC;CAGJ;AAGD,OAAO,EACH,WAAW,GACd,CAAA"}
@@ -0,0 +1,22 @@
1
+ import type { RedisClientType } from "redis";
2
+ import type { RateLimiter } from "./rateLimiter.interface.js";
3
+ type RateLimiterOptions = {
4
+ algorithm: "token-bucket";
5
+ capacity: number;
6
+ refillRate: number;
7
+ client: RedisClientType;
8
+ } | {
9
+ algorithm: "leaky-bucket";
10
+ capacity: number;
11
+ leakRate: number;
12
+ client: RedisClientType;
13
+ } | {
14
+ algorithm: "sliding-window";
15
+ windowSize: number;
16
+ maxRequests: number;
17
+ client: RedisClientType;
18
+ };
19
+ declare function createRateLimiter(options: RateLimiterOptions): RateLimiter;
20
+ export { createRateLimiter };
21
+ export type { RateLimiterOptions };
22
+ //# sourceMappingURL=rateLimiter.factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimiter.factory.d.ts","sourceRoot":"","sources":["../../src/rate_limiter/rateLimiter.factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAK9D,KAAK,kBAAkB,GACjB;IACE,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;CAC3B,GACC;IACE,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,eAAe,CAAC;CAC3B,GACC;IACE,SAAS,EAAE,gBAAgB,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,eAAe,CAAC;CAC3B,CAAC;AAEN,iBAAS,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAuBnE;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,YAAY,EAAE,kBAAkB,EAAE,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { TokenBucket } from "./algorithms/tokenBucket.algo.js";
2
+ import { LeakyBucket } from "./algorithms/leakyBucket.algo.js";
3
+ import { SlidingWindow } from "./algorithms/slidingWindow.algo.js";
4
+ function createRateLimiter(options) {
5
+ switch (options.algorithm) {
6
+ case "token-bucket":
7
+ return new TokenBucket(options.capacity, options.refillRate, options.client);
8
+ case "leaky-bucket":
9
+ return new LeakyBucket(options.capacity, options.leakRate, options.client);
10
+ case "sliding-window":
11
+ return new SlidingWindow(options.windowSize, options.maxRequests, options.client);
12
+ }
13
+ }
14
+ export { createRateLimiter };
15
+ //# sourceMappingURL=rateLimiter.factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimiter.factory.js","sourceRoot":"","sources":["../../src/rate_limiter/rateLimiter.factory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAsBnE,SAAS,iBAAiB,CAAC,OAA2B;IAClD,QAAQ,OAAO,CAAC,SAAS,EAAE,CAAC;QACxB,KAAK,cAAc;YACf,OAAO,IAAI,WAAW,CAClB,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,MAAM,CACjB,CAAC;QAEN,KAAK,cAAc;YACf,OAAO,IAAI,WAAW,CAClB,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,MAAM,CACjB,CAAC;QAEN,KAAK,gBAAgB;YACjB,OAAO,IAAI,aAAa,CACpB,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,WAAW,EACnB,OAAO,CAAC,MAAM,CACjB,CAAC;IACV,CAAC;AACL,CAAC;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
@@ -0,0 +1,5 @@
1
+ interface RateLimiter {
2
+ allowRequest(userId: string): Promise<boolean>;
3
+ }
4
+ export type { RateLimiter };
5
+ //# sourceMappingURL=rateLimiter.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimiter.interface.d.ts","sourceRoot":"","sources":["../../src/rate_limiter/rateLimiter.interface.ts"],"names":[],"mappings":"AAAA,UAAU,WAAW;IACjB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAClD;AAED,YAAY,EAAE,WAAW,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rateLimiter.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimiter.interface.js","sourceRoot":"","sources":["../../src/rate_limiter/rateLimiter.interface.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export declare const CACHE_SIMILARITY_THRESHOLD = 0.6;
2
+ //# sourceMappingURL=semantic_cache.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic_cache.config.d.ts","sourceRoot":"","sources":["../../../src/semantic_cache/config/semantic_cache.config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,0BAA0B,MAAM,CAAC"}
@@ -0,0 +1,2 @@
1
+ export const CACHE_SIMILARITY_THRESHOLD = 0.6;
2
+ //# sourceMappingURL=semantic_cache.config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic_cache.config.js","sourceRoot":"","sources":["../../../src/semantic_cache/config/semantic_cache.config.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAG,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { RedisClientType } from "redis";
2
+ import type { LLMProvider } from "../../interfaces/llm.interface.js";
3
+ import type { EmbeddingProvider } from "../../interfaces/embedding.interface.js";
4
+ type SemanticCacheOptions = {
5
+ client: RedisClientType;
6
+ embeddingProvider: EmbeddingProvider;
7
+ llmProvider: LLMProvider;
8
+ threshold?: number;
9
+ };
10
+ declare class SemanticCache {
11
+ private client;
12
+ private embeddingProvider;
13
+ private llmProvider;
14
+ private threshold;
15
+ constructor(options: SemanticCacheOptions);
16
+ private createCacheService;
17
+ private getCacheService;
18
+ private searchCacheService;
19
+ private isCacheHit;
20
+ getCachedOrGenerate(query: string): Promise<string>;
21
+ }
22
+ export { SemanticCache };
23
+ //# sourceMappingURL=semantic_cache.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic_cache.service.d.ts","sourceRoot":"","sources":["../../../src/semantic_cache/services/semantic_cache.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAG7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAIjF,KAAK,oBAAoB,GAAG;IACxB,MAAM,EAAE,eAAe,CAAC;IACxB,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAkBF,cAAM,aAAa;IACf,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,SAAS,CAAS;IAE1B,YAAY,OAAO,EAAE,oBAAoB,EAKxC;YAEa,kBAAkB;YAgBlB,eAAe;YAKf,kBAAkB;IAgDhC,OAAO,CAAC,UAAU;IAIL,mBAAmB,CAAC,KAAK,EAAE,MAAM,mBAY7C;CACJ;AAED,OAAO,EACH,aAAa,EAChB,CAAC"}
@@ -0,0 +1,82 @@
1
+ import { embeddingToBuffer } from "../utils/vector.utils.js";
2
+ import crypto from "crypto";
3
+ import { CACHE_SIMILARITY_THRESHOLD } from "../config/semantic_cache.config.js";
4
+ class SemanticCache {
5
+ client;
6
+ embeddingProvider;
7
+ llmProvider;
8
+ threshold;
9
+ constructor(options) {
10
+ this.client = options.client;
11
+ this.embeddingProvider = options.embeddingProvider;
12
+ this.llmProvider = options.llmProvider;
13
+ this.threshold = options.threshold ?? CACHE_SIMILARITY_THRESHOLD;
14
+ }
15
+ async createCacheService(query, response) {
16
+ const cacheId = crypto.randomUUID();
17
+ const embedding = await this.embeddingProvider.generateEmbedding(query);
18
+ const embeddingBuffer = embeddingToBuffer(embedding);
19
+ await this.client.hSet(`semantic_cache:${cacheId}`, {
20
+ query,
21
+ response,
22
+ embedding: embeddingBuffer,
23
+ });
24
+ }
25
+ async getCacheService(cache_id, client) {
26
+ const cacheEntry = await client.hGetAll(`semantic_cache:${cache_id}`);
27
+ return cacheEntry;
28
+ }
29
+ async searchCacheService(query) {
30
+ const embedding = await this.embeddingProvider.generateEmbedding(query);
31
+ const embeddingBuffer = embeddingToBuffer(embedding);
32
+ const result = await this.client.sendCommand([
33
+ "FT.SEARCH",
34
+ "semantic_cache_idx",
35
+ "*=>[KNN 1 @embedding $query_vector AS vector_score]",
36
+ "PARAMS",
37
+ "2",
38
+ "query_vector",
39
+ embeddingBuffer,
40
+ "SORTBY",
41
+ "vector_score",
42
+ "DIALECT",
43
+ "2",
44
+ "RETURN",
45
+ "3",
46
+ "query",
47
+ "response",
48
+ "vector_score",
49
+ ]);
50
+ if (result.total_results === 0) {
51
+ return null;
52
+ }
53
+ const cacheEntry = result.results[0];
54
+ if (!cacheEntry) {
55
+ return null;
56
+ }
57
+ const distance = Number(cacheEntry.extra_attributes.vector_score);
58
+ const similarityScore = Math.max(0, Math.min(1, 1 - distance));
59
+ return {
60
+ id: cacheEntry.id,
61
+ query: cacheEntry.extra_attributes.query,
62
+ response: cacheEntry.extra_attributes.response,
63
+ similarityScore: similarityScore,
64
+ };
65
+ }
66
+ isCacheHit(similarityScore) {
67
+ return similarityScore >= this.threshold;
68
+ }
69
+ async getCachedOrGenerate(query) {
70
+ const cacheEntry = await this.searchCacheService(query);
71
+ if (cacheEntry && this.isCacheHit(cacheEntry.similarityScore)) {
72
+ console.log("cachehit");
73
+ return cacheEntry.response;
74
+ }
75
+ console.log("cachemiss");
76
+ const response = await this.llmProvider.askLLM(query);
77
+ await this.createCacheService(query, response);
78
+ return response;
79
+ }
80
+ }
81
+ export { SemanticCache };
82
+ //# sourceMappingURL=semantic_cache.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic_cache.service.js","sourceRoot":"","sources":["../../../src/semantic_cache/services/semantic_cache.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
2
+ //# sourceMappingURL=similarity.utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"similarity.utils.d.ts","sourceRoot":"","sources":["../../../src/semantic_cache/utils/similarity.utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAC5B,CAAC,EAAE,MAAM,EAAE,EACX,CAAC,EAAE,MAAM,EAAE,GACZ,MAAM,CAoBR"}
@@ -0,0 +1,18 @@
1
+ export function cosineSimilarity(a, b) {
2
+ if (a.length !== b.length) {
3
+ throw new Error("Vectors must have the same dimensions");
4
+ }
5
+ let dotProduct = 0;
6
+ let magnitudeA = 0;
7
+ let magnitudeB = 0;
8
+ for (let i = 0; i < a.length; i++) {
9
+ dotProduct += a[i] * b[i];
10
+ magnitudeA += a[i] * a[i];
11
+ magnitudeB += b[i] * b[i];
12
+ }
13
+ if (magnitudeA === 0 || magnitudeB === 0) {
14
+ throw new Error("Cannot calculate similarity for zero vectors");
15
+ }
16
+ return dotProduct / (Math.sqrt(magnitudeA) * Math.sqrt(magnitudeB));
17
+ }
18
+ //# sourceMappingURL=similarity.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"similarity.utils.js","sourceRoot":"","sources":["../../../src/semantic_cache/utils/similarity.utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAC5B,CAAW,EACX,CAAW;IAEX,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,UAAU,IAAI,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QAC5B,UAAU,IAAI,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QAC5B,UAAU,IAAI,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;IAChC,CAAC;IAED,IAAI,UAAU,KAAK,CAAC,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,UAAU,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;AACxE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function embeddingToBuffer(embedding: number[]): Buffer;
2
+ //# sourceMappingURL=vector.utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vector.utils.d.ts","sourceRoot":"","sources":["../../../src/semantic_cache/utils/vector.utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAI7D"}
@@ -0,0 +1,5 @@
1
+ export function embeddingToBuffer(embedding) {
2
+ const float32Embedding = new Float32Array(embedding);
3
+ return Buffer.from(float32Embedding.buffer);
4
+ }
5
+ //# sourceMappingURL=vector.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vector.utils.js","sourceRoot":"","sources":["../../../src/semantic_cache/utils/vector.utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB,CAAC,SAAmB;IACjD,MAAM,gBAAgB,GAAG,IAAI,YAAY,CAAC,SAAS,CAAC,CAAC;IAErD,OAAO,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@rupayan-das/request-guard",
3
+ "version": "1.0.0",
4
+ "description": "Redis-based rate limiting and semantic caching for Node.js applications",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "test": "vitest",
19
+ "build": "tsc",
20
+ "clean": "rimraf dist",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "rate-limiter",
25
+ "rate-limit",
26
+ "redis",
27
+ "semantic-cache",
28
+ "typescript"
29
+ ],
30
+ "author": "Rupayan Das",
31
+ "license": "MIT",
32
+ "devDependencies": {
33
+ "@types/node": "^26.4.0",
34
+ "rimraf": "^6.1.3",
35
+ "typescript": "^7.0.2",
36
+ "vitest": "^4.1.11"
37
+ },
38
+ "dependencies": {
39
+ "redis": "^6.2.1"
40
+ },
41
+ "engines": {
42
+ "node": ">=20"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/Rupayan-san/request-guard.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/Rupayan-san/request-guard/issues"
50
+ },
51
+ "homepage": "https://github.com/Rupayan-san/request-guard#readme"
52
+ }