@abinashpatri/cache 1.0.0 → 1.0.1
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/README.md +157 -1
- package/dist/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ Built for simple integration with:
|
|
|
6
6
|
- safe JSON serialization/deserialization
|
|
7
7
|
- TTL with jitter to reduce cache stampedes
|
|
8
8
|
- auto-cache wrapper (`remember`)
|
|
9
|
+
- Redis-backed Express rate limiting (`express-rate-limit` + Redis store)
|
|
9
10
|
- ESM + CommonJS support
|
|
10
11
|
- full TypeScript typings
|
|
11
12
|
|
|
@@ -123,12 +124,166 @@ const posts = await remember("posts:home", 30, async () => {
|
|
|
123
124
|
});
|
|
124
125
|
```
|
|
125
126
|
|
|
127
|
+
### `createRedisRateLimiter(options?): RateLimitRequestHandler`
|
|
128
|
+
|
|
129
|
+
Creates an Express middleware that stores rate-limit counters in Redis.
|
|
130
|
+
|
|
131
|
+
Defaults:
|
|
132
|
+
- `windowMs`: `15 * 60 * 1000` (15 minutes)
|
|
133
|
+
- `limit`: `100`
|
|
134
|
+
- `standardHeaders`: `"draft-7"`
|
|
135
|
+
- `legacyHeaders`: `false`
|
|
136
|
+
- Redis key `prefix`: `"rl:"`
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import express from "express";
|
|
140
|
+
import { connect, createRedisRateLimiter } from "@abinashpatri/cache";
|
|
141
|
+
|
|
142
|
+
const app = express();
|
|
143
|
+
|
|
144
|
+
await connect(process.env.REDIS_URL);
|
|
145
|
+
|
|
146
|
+
const limiter = createRedisRateLimiter({
|
|
147
|
+
windowMs: 15 * 60 * 1000,
|
|
148
|
+
limit: 100,
|
|
149
|
+
prefix: "rate-limit:",
|
|
150
|
+
message: {
|
|
151
|
+
success: false,
|
|
152
|
+
message: "Too many requests, please try again later.",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
app.use("/api", limiter);
|
|
157
|
+
|
|
158
|
+
app.get("/api/health", (_req, res) => {
|
|
159
|
+
res.json({ ok: true });
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Important: call `connect()` before creating or using the limiter so Redis is ready.
|
|
164
|
+
|
|
165
|
+
## Rate Limit Patterns (Express)
|
|
166
|
+
|
|
167
|
+
### 1) Global limiter (all routes)
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const globalLimiter = createRedisRateLimiter({
|
|
171
|
+
windowMs: 15 * 60 * 1000,
|
|
172
|
+
limit: 300,
|
|
173
|
+
prefix: "rl:global:",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
app.use(globalLimiter);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 2) Router-level limiter (only `/api`)
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const apiLimiter = createRedisRateLimiter({
|
|
183
|
+
windowMs: 60 * 1000,
|
|
184
|
+
limit: 100,
|
|
185
|
+
prefix: "rl:api:",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
app.use("/api", apiLimiter);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 3) Route-level limiter (single endpoint)
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const loginLimiter = createRedisRateLimiter({
|
|
195
|
+
windowMs: 10 * 60 * 1000,
|
|
196
|
+
limit: 5,
|
|
197
|
+
prefix: "rl:login:",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
app.post("/auth/login", loginLimiter, loginHandler);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 4) Different limits for different routes
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const strictLimiter = createRedisRateLimiter({ windowMs: 60 * 1000, limit: 20, prefix: "rl:strict:" });
|
|
207
|
+
const relaxedLimiter = createRedisRateLimiter({ windowMs: 60 * 1000, limit: 200, prefix: "rl:relaxed:" });
|
|
208
|
+
|
|
209
|
+
app.use("/auth", strictLimiter);
|
|
210
|
+
app.use("/public", relaxedLimiter);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 5) Per-user/API key rate limit (custom key generator)
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const userLimiter = createRedisRateLimiter({
|
|
217
|
+
windowMs: 60 * 1000,
|
|
218
|
+
limit: 60,
|
|
219
|
+
prefix: "rl:user:",
|
|
220
|
+
keyGenerator: (req) => {
|
|
221
|
+
const userId = req.headers["x-user-id"];
|
|
222
|
+
if (typeof userId === "string" && userId.length > 0) return `user:${userId}`;
|
|
223
|
+
return req.ip ?? "unknown";
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
app.use("/v1", userLimiter);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 6) Skip successful requests (limit only failed attempts)
|
|
231
|
+
|
|
232
|
+
Useful for brute-force protection on auth endpoints.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const authLimiter = createRedisRateLimiter({
|
|
236
|
+
windowMs: 15 * 60 * 1000,
|
|
237
|
+
limit: 10,
|
|
238
|
+
prefix: "rl:auth:",
|
|
239
|
+
skipSuccessfulRequests: true,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
app.post("/auth/login", authLimiter, loginHandler);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 7) Custom 429 response handler
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const customHandlerLimiter = createRedisRateLimiter({
|
|
249
|
+
windowMs: 60 * 1000,
|
|
250
|
+
limit: 50,
|
|
251
|
+
prefix: "rl:custom:",
|
|
252
|
+
handler: (req, res) => {
|
|
253
|
+
res.status(429).json({
|
|
254
|
+
success: false,
|
|
255
|
+
message: "Rate limit exceeded",
|
|
256
|
+
route: req.originalUrl,
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
app.use("/api", customHandlerLimiter);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### 8) Proxy/LB deployment setup (important)
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
app.set("trust proxy", 1);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Set this when running behind Nginx, Cloudflare, Render, Railway, Heroku, or similar proxy/CDN, so client IP based limiting is correct.
|
|
271
|
+
|
|
126
272
|
## Import Styles
|
|
127
273
|
|
|
128
274
|
### Named imports (recommended)
|
|
129
275
|
|
|
130
276
|
```ts
|
|
131
|
-
import {
|
|
277
|
+
import {
|
|
278
|
+
connect,
|
|
279
|
+
get,
|
|
280
|
+
set,
|
|
281
|
+
del,
|
|
282
|
+
remember,
|
|
283
|
+
ping,
|
|
284
|
+
disconnect,
|
|
285
|
+
createRedisRateLimiter,
|
|
286
|
+
} from "@abinashpatri/cache";
|
|
132
287
|
```
|
|
133
288
|
|
|
134
289
|
### Default import
|
|
@@ -138,6 +293,7 @@ import cache from "@abinashpatri/cache";
|
|
|
138
293
|
|
|
139
294
|
await cache.connect();
|
|
140
295
|
await cache.set("k", { ok: true }, 60);
|
|
296
|
+
const limiter = cache.createRedisRateLimiter({ limit: 50 });
|
|
141
297
|
```
|
|
142
298
|
|
|
143
299
|
## Production Usage Notes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { Options, RateLimitRequestHandler } from 'express-rate-limit';
|
|
2
|
+
|
|
3
|
+
type RedisRateLimitOptions = Partial<Omit<Options, "store">> & {
|
|
4
|
+
prefix?: string;
|
|
5
|
+
resetExpiryOnChange?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare function createRedisRateLimiter(options?: RedisRateLimitOptions): RateLimitRequestHandler;
|
|
8
|
+
|
|
1
9
|
declare function get<T = unknown>(key: string): Promise<T | null>;
|
|
2
10
|
declare function set<T>(key: string, value: T, ttl?: number): Promise<void>;
|
|
3
11
|
declare function del(key: string): Promise<void>;
|
|
@@ -15,6 +23,7 @@ declare const redis: {
|
|
|
15
23
|
connect: typeof connect;
|
|
16
24
|
disconnect: typeof disconnect;
|
|
17
25
|
ping: typeof ping;
|
|
26
|
+
createRedisRateLimiter: typeof createRedisRateLimiter;
|
|
18
27
|
};
|
|
19
28
|
|
|
20
|
-
export { connect, redis as default, del, disconnect, get, ping, remember, set };
|
|
29
|
+
export { type RedisRateLimitOptions, connect, createRedisRateLimiter, redis as default, del, disconnect, get, ping, remember, set };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { Options, RateLimitRequestHandler } from 'express-rate-limit';
|
|
2
|
+
|
|
3
|
+
type RedisRateLimitOptions = Partial<Omit<Options, "store">> & {
|
|
4
|
+
prefix?: string;
|
|
5
|
+
resetExpiryOnChange?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare function createRedisRateLimiter(options?: RedisRateLimitOptions): RateLimitRequestHandler;
|
|
8
|
+
|
|
1
9
|
declare function get<T = unknown>(key: string): Promise<T | null>;
|
|
2
10
|
declare function set<T>(key: string, value: T, ttl?: number): Promise<void>;
|
|
3
11
|
declare function del(key: string): Promise<void>;
|
|
@@ -15,6 +23,7 @@ declare const redis: {
|
|
|
15
23
|
connect: typeof connect;
|
|
16
24
|
disconnect: typeof disconnect;
|
|
17
25
|
ping: typeof ping;
|
|
26
|
+
createRedisRateLimiter: typeof createRedisRateLimiter;
|
|
18
27
|
};
|
|
19
28
|
|
|
20
|
-
export { connect, redis as default, del, disconnect, get, ping, remember, set };
|
|
29
|
+
export { type RedisRateLimitOptions, connect, createRedisRateLimiter, redis as default, del, disconnect, get, ping, remember, set };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var H=Object.create;var a=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var q=Object.getPrototypeOf,E=Object.prototype.hasOwnProperty;var R=(e,t)=>{for(var n in t)a(e,n,{get:t[n],enumerable:!0})},y=(e,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of b(t))!E.call(e,r)&&r!==n&&a(e,r,{get:()=>t[r],enumerable:!(i=v(t,r))||i.enumerable});return e};var M=(e,t,n)=>(n=e!=null?H(q(e)):{},y(t||!e||!e.__esModule?a(n,"default",{value:e,enumerable:!0}):n,e)),J=e=>y(a({},"__esModule",{value:!0}),e);var W={};R(W,{connect:()=>P,createRedisRateLimiter:()=>m,default:()=>L,del:()=>f,disconnect:()=>O,get:()=>c,ping:()=>h,remember:()=>u,set:()=>l});module.exports=J(W);var x=require("redis"),o=null;async function C(e="redis://127.0.0.1:6379"){return o||(o=(0,x.createClient)({url:e}),o.on("connect",()=>{console.log("Redis connected")}),o.on("error",t=>{console.error("Redis error:",t)})),o.isOpen||await o.connect(),o}function s(){if(!o)throw new Error("Redis not connected. Call connect() first.");return o}var g={};R(g,{del:()=>f,get:()=>c,remember:()=>u,set:()=>l});function S(e){try{return JSON.parse(e)}catch{return null}}async function c(e){let n=await s().get(e);return n?S(n):null}async function l(e,t,n=60){let i=s(),r=n+Math.floor(Math.random()*10);await i.set(e,JSON.stringify(t),{EX:r})}async function f(e){await s().del(e)}async function u(e,t,n){let i=await c(e);if(i!==null)return console.log("Cache hit:",e),i;console.log("Cache miss:",e);let r=await n();return await l(e,r,t),r}var w=M(require("express-rate-limit")),T=require("rate-limit-redis");function m(e={}){let{prefix:t="rl:",resetExpiryOnChange:n=!1,...i}=e;return(0,w.default)({windowMs:i.windowMs??900*1e3,limit:i.limit??100,standardHeaders:i.standardHeaders??"draft-7",legacyHeaders:i.legacyHeaders??!1,message:i.message??"Too many requests, please try again later.",...i,store:new T.RedisStore({prefix:t,resetExpiryOnChange:n,sendCommand:(...r)=>s().sendCommand(r)})})}var p=!1,d=null;async function P(e){if(!p)return d||(d=C(e).then(()=>{p=!0})),d}async function h(){return s().ping()}async function O(){if(!p)return;await s().quit(),p=!1,d=null}var N={connect:P,disconnect:O,ping:h,createRedisRateLimiter:m,...g},L=N;0&&(module.exports={connect,createRedisRateLimiter,del,disconnect,get,ping,remember,set});
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/redis/connection.ts","../src/redis/cache.ts","../src/redis/index.ts"],"sourcesContent":["export * from \"./redis/index\";\nexport { default } from \"./redis/index\";\n","import { createClient, type RedisClientType } from \"redis\";\n\nlet client: RedisClientType | null = null;\n\nexport async function connect(url = \"redis://127.0.0.1:6379\"): Promise<RedisClientType> {\n if (!client) {\n client = createClient({ url });\n\n client.on(\"connect\", () => {\n console.log(\"Redis connected\");\n });\n\n client.on(\"error\", (err: unknown) => {\n console.error(\"Redis error:\", err);\n });\n }\n\n if (!client.isOpen) {\n await client.connect();\n }\n\n return client;\n}\n\nexport function getClient(): RedisClientType {\n if (!client) {\n throw new Error(\"Redis not connected. Call connect() first.\");\n }\n\n return client;\n}\n","import { getClient } from \"./connection\";\n\nfunction safeParse<T>(data: string): T | null {\n try {\n return JSON.parse(data) as T;\n } catch {\n return null;\n }\n}\n\nexport async function get<T = unknown>(key: string): Promise<T | null> {\n const client = getClient();\n const data = await client.get(key);\n\n return data ? safeParse<T>(data) : null;\n}\n\nexport async function set<T>(key: string, value: T, ttl = 60): Promise<void> {\n const client = getClient();\n const ttlWithJitter = ttl + Math.floor(Math.random() * 10);\n\n await client.set(key, JSON.stringify(value), {\n EX: ttlWithJitter,\n });\n}\n\nexport async function del(key: string): Promise<void> {\n const client = getClient();\n await client.del(key);\n}\n\nexport async function remember<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T> {\n const cached = await get<T>(key);\n\n if (cached !== null) {\n console.log(\"Cache hit:\", key);\n return cached;\n }\n\n console.log(\"Cache miss:\", key);\n\n const result = await fn();\n await set(key, result, ttl);\n\n return result;\n}\n","import { connect as connectClient, getClient } from \"./connection\";\nimport * as cache from \"./cache\";\n\nlet isConnected = false;\nlet connecting: Promise<void> | null = null;\n\nexport async function connect(url?: string): Promise<void> {\n if (isConnected) {\n return;\n }\n\n if (!connecting) {\n connecting = connectClient(url).then(() => {\n isConnected = true;\n });\n }\n\n return connecting;\n}\n\nexport async function ping(): Promise<string> {\n const client = getClient();\n return client.ping();\n}\n\nexport async function disconnect(): Promise<void> {\n if (!isConnected) {\n return;\n }\n\n const client = getClient();\n await client.quit();\n\n isConnected = false;\n connecting = null;\n}\n\nexport { get, set, del, remember } from \"./cache\";\n\nconst redis = {\n connect,\n disconnect,\n ping,\n ...cache,\n};\n\nexport default redis;\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/redis/connection.ts","../src/redis/cache.ts","../src/rate-limit.ts","../src/redis/index.ts"],"sourcesContent":["export * from \"./redis/index\";\nexport { default } from \"./redis/index\";\nexport * from \"./rate-limit\";\n","import { createClient, type RedisClientType } from \"redis\";\n\nlet client: RedisClientType | null = null;\n\nexport async function connect(url = \"redis://127.0.0.1:6379\"): Promise<RedisClientType> {\n if (!client) {\n client = createClient({ url });\n\n client.on(\"connect\", () => {\n console.log(\"Redis connected\");\n });\n\n client.on(\"error\", (err: unknown) => {\n console.error(\"Redis error:\", err);\n });\n }\n\n if (!client.isOpen) {\n await client.connect();\n }\n\n return client;\n}\n\nexport function getClient(): RedisClientType {\n if (!client) {\n throw new Error(\"Redis not connected. Call connect() first.\");\n }\n\n return client;\n}\n","import { getClient } from \"./connection\";\n\nfunction safeParse<T>(data: string): T | null {\n try {\n return JSON.parse(data) as T;\n } catch {\n return null;\n }\n}\n\nexport async function get<T = unknown>(key: string): Promise<T | null> {\n const client = getClient();\n const data = await client.get(key);\n\n return data ? safeParse<T>(data) : null;\n}\n\nexport async function set<T>(key: string, value: T, ttl = 60): Promise<void> {\n const client = getClient();\n const ttlWithJitter = ttl + Math.floor(Math.random() * 10);\n\n await client.set(key, JSON.stringify(value), {\n EX: ttlWithJitter,\n });\n}\n\nexport async function del(key: string): Promise<void> {\n const client = getClient();\n await client.del(key);\n}\n\nexport async function remember<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T> {\n const cached = await get<T>(key);\n\n if (cached !== null) {\n console.log(\"Cache hit:\", key);\n return cached;\n }\n\n console.log(\"Cache miss:\", key);\n\n const result = await fn();\n await set(key, result, ttl);\n\n return result;\n}\n","import rateLimit, { type Options, type RateLimitRequestHandler } from \"express-rate-limit\";\nimport { RedisStore, type RedisReply } from \"rate-limit-redis\";\nimport { getClient } from \"./redis/connection\";\n\nexport type RedisRateLimitOptions = Partial<Omit<Options, \"store\">> & {\n prefix?: string;\n resetExpiryOnChange?: boolean;\n};\n\nexport function createRedisRateLimiter(options: RedisRateLimitOptions = {}): RateLimitRequestHandler {\n const { prefix = \"rl:\", resetExpiryOnChange = false, ...rateLimitOptions } = options;\n\n return rateLimit({\n windowMs: rateLimitOptions.windowMs ?? 15 * 60 * 1000,\n limit: rateLimitOptions.limit ?? 100,\n standardHeaders: rateLimitOptions.standardHeaders ?? \"draft-7\",\n legacyHeaders: rateLimitOptions.legacyHeaders ?? false,\n message: rateLimitOptions.message ?? \"Too many requests, please try again later.\",\n ...rateLimitOptions,\n store: new RedisStore({\n prefix,\n resetExpiryOnChange,\n sendCommand: (...args: string[]): Promise<RedisReply> =>\n getClient().sendCommand(args) as Promise<RedisReply>,\n }),\n });\n}\n","import { connect as connectClient, getClient } from \"./connection\";\nimport * as cache from \"./cache\";\nimport { createRedisRateLimiter } from \"../rate-limit\";\n\nlet isConnected = false;\nlet connecting: Promise<void> | null = null;\n\nexport async function connect(url?: string): Promise<void> {\n if (isConnected) {\n return;\n }\n\n if (!connecting) {\n connecting = connectClient(url).then(() => {\n isConnected = true;\n });\n }\n\n return connecting;\n}\n\nexport async function ping(): Promise<string> {\n const client = getClient();\n return client.ping();\n}\n\nexport async function disconnect(): Promise<void> {\n if (!isConnected) {\n return;\n }\n\n const client = getClient();\n await client.quit();\n\n isConnected = false;\n connecting = null;\n}\n\nexport { get, set, del, remember } from \"./cache\";\nexport { createRedisRateLimiter } from \"../rate-limit\";\n\nconst redis = {\n connect,\n disconnect,\n ping,\n createRedisRateLimiter,\n ...cache,\n};\n\nexport default redis;\n"],"mappings":"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","names":["index_exports","__export","connect","createRedisRateLimiter","redis_default","del","disconnect","get","ping","remember","set","__toCommonJS","import_redis","client","connect","url","err","getClient","cache_exports","__export","del","get","remember","set","safeParse","data","get","key","getClient","set","value","ttl","client","ttlWithJitter","del","remember","fn","cached","result","import_express_rate_limit","import_rate_limit_redis","createRedisRateLimiter","options","prefix","resetExpiryOnChange","rateLimitOptions","rateLimit","args","getClient","isConnected","connecting","connect","url","ping","getClient","disconnect","redis","createRedisRateLimiter","cache_exports","redis_default"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var R=Object.defineProperty;var y=(e,t)=>{for(var n in t)R(e,n,{get:t[n],enumerable:!0})};import{createClient as x}from"redis";var r=null;async function f(e="redis://127.0.0.1:6379"){return r||(r=x({url:e}),r.on("connect",()=>{console.log("Redis connected")}),r.on("error",t=>{console.error("Redis error:",t)})),r.isOpen||await r.connect(),r}function o(){if(!r)throw new Error("Redis not connected. Call connect() first.");return r}var d={};y(d,{del:()=>u,get:()=>l,remember:()=>g,set:()=>m});function C(e){try{return JSON.parse(e)}catch{return null}}async function l(e){let n=await o().get(e);return n?C(n):null}async function m(e,t,n=60){let i=o(),s=n+Math.floor(Math.random()*10);await i.set(e,JSON.stringify(t),{EX:s})}async function u(e){await o().del(e)}async function g(e,t,n){let i=await l(e);if(i!==null)return console.log("Cache hit:",e),i;console.log("Cache miss:",e);let s=await n();return await m(e,s,t),s}import w from"express-rate-limit";import{RedisStore as T}from"rate-limit-redis";function p(e={}){let{prefix:t="rl:",resetExpiryOnChange:n=!1,...i}=e;return w({windowMs:i.windowMs??900*1e3,limit:i.limit??100,standardHeaders:i.standardHeaders??"draft-7",legacyHeaders:i.legacyHeaders??!1,message:i.message??"Too many requests, please try again later.",...i,store:new T({prefix:t,resetExpiryOnChange:n,sendCommand:(...s)=>o().sendCommand(s)})})}var c=!1,a=null;async function P(e){if(!c)return a||(a=f(e).then(()=>{c=!0})),a}async function h(){return o().ping()}async function O(){if(!c)return;await o().quit(),c=!1,a=null}var L={connect:P,disconnect:O,ping:h,createRedisRateLimiter:p,...d},H=L;export{P as connect,p as createRedisRateLimiter,H as default,u as del,O as disconnect,l as get,h as ping,g as remember,m as set};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/redis/connection.ts","../src/redis/cache.ts","../src/redis/index.ts"],"sourcesContent":["import { createClient, type RedisClientType } from \"redis\";\n\nlet client: RedisClientType | null = null;\n\nexport async function connect(url = \"redis://127.0.0.1:6379\"): Promise<RedisClientType> {\n if (!client) {\n client = createClient({ url });\n\n client.on(\"connect\", () => {\n console.log(\"Redis connected\");\n });\n\n client.on(\"error\", (err: unknown) => {\n console.error(\"Redis error:\", err);\n });\n }\n\n if (!client.isOpen) {\n await client.connect();\n }\n\n return client;\n}\n\nexport function getClient(): RedisClientType {\n if (!client) {\n throw new Error(\"Redis not connected. Call connect() first.\");\n }\n\n return client;\n}\n","import { getClient } from \"./connection\";\n\nfunction safeParse<T>(data: string): T | null {\n try {\n return JSON.parse(data) as T;\n } catch {\n return null;\n }\n}\n\nexport async function get<T = unknown>(key: string): Promise<T | null> {\n const client = getClient();\n const data = await client.get(key);\n\n return data ? safeParse<T>(data) : null;\n}\n\nexport async function set<T>(key: string, value: T, ttl = 60): Promise<void> {\n const client = getClient();\n const ttlWithJitter = ttl + Math.floor(Math.random() * 10);\n\n await client.set(key, JSON.stringify(value), {\n EX: ttlWithJitter,\n });\n}\n\nexport async function del(key: string): Promise<void> {\n const client = getClient();\n await client.del(key);\n}\n\nexport async function remember<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T> {\n const cached = await get<T>(key);\n\n if (cached !== null) {\n console.log(\"Cache hit:\", key);\n return cached;\n }\n\n console.log(\"Cache miss:\", key);\n\n const result = await fn();\n await set(key, result, ttl);\n\n return result;\n}\n","import { connect as connectClient, getClient } from \"./connection\";\nimport * as cache from \"./cache\";\n\nlet isConnected = false;\nlet connecting: Promise<void> | null = null;\n\nexport async function connect(url?: string): Promise<void> {\n if (isConnected) {\n return;\n }\n\n if (!connecting) {\n connecting = connectClient(url).then(() => {\n isConnected = true;\n });\n }\n\n return connecting;\n}\n\nexport async function ping(): Promise<string> {\n const client = getClient();\n return client.ping();\n}\n\nexport async function disconnect(): Promise<void> {\n if (!isConnected) {\n return;\n }\n\n const client = getClient();\n await client.quit();\n\n isConnected = false;\n connecting = null;\n}\n\nexport { get, set, del, remember } from \"./cache\";\n\nconst redis = {\n connect,\n disconnect,\n ping,\n ...cache,\n};\n\nexport default redis;\n"],"mappings":"0FAAA,OAAS,gBAAAA,MAA0C,QAEnD,IAAIC,EAAiC,KAErC,eAAsBC,EAAQC,EAAM,yBAAoD,CACtF,OAAKF,IACHA,EAASD,EAAa,CAAE,IAAAG,CAAI,CAAC,EAE7BF,EAAO,GAAG,UAAW,IAAM,CACzB,QAAQ,IAAI,iBAAiB,CAC/B,CAAC,EAEDA,EAAO,GAAG,QAAUG,GAAiB,CACnC,QAAQ,MAAM,eAAgBA,CAAG,CACnC,CAAC,GAGEH,EAAO,QACV,MAAMA,EAAO,QAAQ,EAGhBA,CACT,CAEO,SAASI,GAA6B,CAC3C,GAAI,CAACJ,EACH,MAAM,IAAI,MAAM,4CAA4C,EAG9D,OAAOA,CACT,CC9BA,IAAAK,EAAA,GAAAC,EAAAD,EAAA,SAAAE,EAAA,QAAAC,EAAA,aAAAC,EAAA,QAAAC,IAEA,SAASC,EAAaC,EAAwB,CAC5C,GAAI,CACF,OAAO,KAAK,MAAMA,CAAI,CACxB,MAAQ,CACN,OAAO,IACT,CACF,CAEA,eAAsBC,EAAiBC,EAAgC,CAErE,IAAMF,EAAO,MADEG,EAAU,EACC,IAAID,CAAG,EAEjC,OAAOF,EAAOD,EAAaC,CAAI,EAAI,IACrC,CAEA,eAAsBI,EAAOF,EAAaG,EAAUC,EAAM,GAAmB,CAC3E,IAAMC,EAASJ,EAAU,EACnBK,EAAgBF,EAAM,KAAK,MAAM,KAAK,OAAO,EAAI,EAAE,EAEzD,MAAMC,EAAO,IAAIL,EAAK,KAAK,UAAUG,CAAK,EAAG,CAC3C,GAAIG,CACN,CAAC,CACH,CAEA,eAAsBC,EAAIP,EAA4B,CAEpD,MADeC,EAAU,EACZ,IAAID,CAAG,CACtB,CAEA,eAAsBQ,EAAYR,EAAaI,EAAaK,EAAkC,CAC5F,IAAMC,EAAS,MAAMX,EAAOC,CAAG,EAE/B,GAAIU,IAAW,KACb,eAAQ,IAAI,aAAcV,CAAG,EACtBU,EAGT,QAAQ,IAAI,cAAeV,CAAG,EAE9B,IAAMW,EAAS,MAAMF,EAAG,EACxB,aAAMP,EAAIF,EAAKW,EAAQP,CAAG,EAEnBO,CACT,
|
|
1
|
+
{"version":3,"sources":["../src/redis/connection.ts","../src/redis/cache.ts","../src/rate-limit.ts","../src/redis/index.ts"],"sourcesContent":["import { createClient, type RedisClientType } from \"redis\";\n\nlet client: RedisClientType | null = null;\n\nexport async function connect(url = \"redis://127.0.0.1:6379\"): Promise<RedisClientType> {\n if (!client) {\n client = createClient({ url });\n\n client.on(\"connect\", () => {\n console.log(\"Redis connected\");\n });\n\n client.on(\"error\", (err: unknown) => {\n console.error(\"Redis error:\", err);\n });\n }\n\n if (!client.isOpen) {\n await client.connect();\n }\n\n return client;\n}\n\nexport function getClient(): RedisClientType {\n if (!client) {\n throw new Error(\"Redis not connected. Call connect() first.\");\n }\n\n return client;\n}\n","import { getClient } from \"./connection\";\n\nfunction safeParse<T>(data: string): T | null {\n try {\n return JSON.parse(data) as T;\n } catch {\n return null;\n }\n}\n\nexport async function get<T = unknown>(key: string): Promise<T | null> {\n const client = getClient();\n const data = await client.get(key);\n\n return data ? safeParse<T>(data) : null;\n}\n\nexport async function set<T>(key: string, value: T, ttl = 60): Promise<void> {\n const client = getClient();\n const ttlWithJitter = ttl + Math.floor(Math.random() * 10);\n\n await client.set(key, JSON.stringify(value), {\n EX: ttlWithJitter,\n });\n}\n\nexport async function del(key: string): Promise<void> {\n const client = getClient();\n await client.del(key);\n}\n\nexport async function remember<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T> {\n const cached = await get<T>(key);\n\n if (cached !== null) {\n console.log(\"Cache hit:\", key);\n return cached;\n }\n\n console.log(\"Cache miss:\", key);\n\n const result = await fn();\n await set(key, result, ttl);\n\n return result;\n}\n","import rateLimit, { type Options, type RateLimitRequestHandler } from \"express-rate-limit\";\nimport { RedisStore, type RedisReply } from \"rate-limit-redis\";\nimport { getClient } from \"./redis/connection\";\n\nexport type RedisRateLimitOptions = Partial<Omit<Options, \"store\">> & {\n prefix?: string;\n resetExpiryOnChange?: boolean;\n};\n\nexport function createRedisRateLimiter(options: RedisRateLimitOptions = {}): RateLimitRequestHandler {\n const { prefix = \"rl:\", resetExpiryOnChange = false, ...rateLimitOptions } = options;\n\n return rateLimit({\n windowMs: rateLimitOptions.windowMs ?? 15 * 60 * 1000,\n limit: rateLimitOptions.limit ?? 100,\n standardHeaders: rateLimitOptions.standardHeaders ?? \"draft-7\",\n legacyHeaders: rateLimitOptions.legacyHeaders ?? false,\n message: rateLimitOptions.message ?? \"Too many requests, please try again later.\",\n ...rateLimitOptions,\n store: new RedisStore({\n prefix,\n resetExpiryOnChange,\n sendCommand: (...args: string[]): Promise<RedisReply> =>\n getClient().sendCommand(args) as Promise<RedisReply>,\n }),\n });\n}\n","import { connect as connectClient, getClient } from \"./connection\";\nimport * as cache from \"./cache\";\nimport { createRedisRateLimiter } from \"../rate-limit\";\n\nlet isConnected = false;\nlet connecting: Promise<void> | null = null;\n\nexport async function connect(url?: string): Promise<void> {\n if (isConnected) {\n return;\n }\n\n if (!connecting) {\n connecting = connectClient(url).then(() => {\n isConnected = true;\n });\n }\n\n return connecting;\n}\n\nexport async function ping(): Promise<string> {\n const client = getClient();\n return client.ping();\n}\n\nexport async function disconnect(): Promise<void> {\n if (!isConnected) {\n return;\n }\n\n const client = getClient();\n await client.quit();\n\n isConnected = false;\n connecting = null;\n}\n\nexport { get, set, del, remember } from \"./cache\";\nexport { createRedisRateLimiter } from \"../rate-limit\";\n\nconst redis = {\n connect,\n disconnect,\n ping,\n createRedisRateLimiter,\n ...cache,\n};\n\nexport default redis;\n"],"mappings":"0FAAA,OAAS,gBAAAA,MAA0C,QAEnD,IAAIC,EAAiC,KAErC,eAAsBC,EAAQC,EAAM,yBAAoD,CACtF,OAAKF,IACHA,EAASD,EAAa,CAAE,IAAAG,CAAI,CAAC,EAE7BF,EAAO,GAAG,UAAW,IAAM,CACzB,QAAQ,IAAI,iBAAiB,CAC/B,CAAC,EAEDA,EAAO,GAAG,QAAUG,GAAiB,CACnC,QAAQ,MAAM,eAAgBA,CAAG,CACnC,CAAC,GAGEH,EAAO,QACV,MAAMA,EAAO,QAAQ,EAGhBA,CACT,CAEO,SAASI,GAA6B,CAC3C,GAAI,CAACJ,EACH,MAAM,IAAI,MAAM,4CAA4C,EAG9D,OAAOA,CACT,CC9BA,IAAAK,EAAA,GAAAC,EAAAD,EAAA,SAAAE,EAAA,QAAAC,EAAA,aAAAC,EAAA,QAAAC,IAEA,SAASC,EAAaC,EAAwB,CAC5C,GAAI,CACF,OAAO,KAAK,MAAMA,CAAI,CACxB,MAAQ,CACN,OAAO,IACT,CACF,CAEA,eAAsBC,EAAiBC,EAAgC,CAErE,IAAMF,EAAO,MADEG,EAAU,EACC,IAAID,CAAG,EAEjC,OAAOF,EAAOD,EAAaC,CAAI,EAAI,IACrC,CAEA,eAAsBI,EAAOF,EAAaG,EAAUC,EAAM,GAAmB,CAC3E,IAAMC,EAASJ,EAAU,EACnBK,EAAgBF,EAAM,KAAK,MAAM,KAAK,OAAO,EAAI,EAAE,EAEzD,MAAMC,EAAO,IAAIL,EAAK,KAAK,UAAUG,CAAK,EAAG,CAC3C,GAAIG,CACN,CAAC,CACH,CAEA,eAAsBC,EAAIP,EAA4B,CAEpD,MADeC,EAAU,EACZ,IAAID,CAAG,CACtB,CAEA,eAAsBQ,EAAYR,EAAaI,EAAaK,EAAkC,CAC5F,IAAMC,EAAS,MAAMX,EAAOC,CAAG,EAE/B,GAAIU,IAAW,KACb,eAAQ,IAAI,aAAcV,CAAG,EACtBU,EAGT,QAAQ,IAAI,cAAeV,CAAG,EAE9B,IAAMW,EAAS,MAAMF,EAAG,EACxB,aAAMP,EAAIF,EAAKW,EAAQP,CAAG,EAEnBO,CACT,CC7CA,OAAOC,MAA+D,qBACtE,OAAS,cAAAC,MAAmC,mBAQrC,SAASC,EAAuBC,EAAiC,CAAC,EAA4B,CACnG,GAAM,CAAE,OAAAC,EAAS,MAAO,oBAAAC,EAAsB,GAAO,GAAGC,CAAiB,EAAIH,EAE7E,OAAOI,EAAU,CACf,SAAUD,EAAiB,UAAY,IAAU,IACjD,MAAOA,EAAiB,OAAS,IACjC,gBAAiBA,EAAiB,iBAAmB,UACrD,cAAeA,EAAiB,eAAiB,GACjD,QAASA,EAAiB,SAAW,6CACrC,GAAGA,EACH,MAAO,IAAIE,EAAW,CACpB,OAAAJ,EACA,oBAAAC,EACA,YAAa,IAAII,IACfC,EAAU,EAAE,YAAYD,CAAI,CAChC,CAAC,CACH,CAAC,CACH,CCtBA,IAAIE,EAAc,GACdC,EAAmC,KAEvC,eAAsBC,EAAQC,EAA6B,CACzD,GAAI,CAAAH,EAIJ,OAAKC,IACHA,EAAaC,EAAcC,CAAG,EAAE,KAAK,IAAM,CACzCH,EAAc,EAChB,CAAC,GAGIC,CACT,CAEA,eAAsBG,GAAwB,CAE5C,OADeC,EAAU,EACX,KAAK,CACrB,CAEA,eAAsBC,GAA4B,CAChD,GAAI,CAACN,EACH,OAIF,MADeK,EAAU,EACZ,KAAK,EAElBL,EAAc,GACdC,EAAa,IACf,CAKA,IAAMM,EAAQ,CACZ,QAAAL,EACA,WAAAI,EACA,KAAAF,EACA,uBAAAI,EACA,GAAGC,CACL,EAEOC,EAAQH","names":["createClient","client","connect","url","err","getClient","cache_exports","__export","del","get","remember","set","safeParse","data","get","key","getClient","set","value","ttl","client","ttlWithJitter","del","remember","fn","cached","result","rateLimit","RedisStore","createRedisRateLimiter","options","prefix","resetExpiryOnChange","rateLimitOptions","rateLimit","RedisStore","args","getClient","isConnected","connecting","connect","url","ping","getClient","disconnect","redis","createRedisRateLimiter","cache_exports","redis_default"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abinashpatri/cache",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Production-grade cache & rate limit utility library",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
"typescript": "^5.9.3"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"express-rate-limit": "^8.3.1",
|
|
41
|
+
"rate-limit-redis": "^4.3.1",
|
|
40
42
|
"redis": "^5.11.0"
|
|
41
43
|
}
|
|
42
44
|
}
|