nextlimiter 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextlimiter",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Production-ready rate limiting for Node.js — sliding window, token bucket, SaaS plans, smart limiting, and built-in analytics.",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -67,4 +67,4 @@
67
67
  "url": "https://github.com/abhishekck31/nextlimiter/issues"
68
68
  },
69
69
  "homepage": "https://github.com/abhishekck31/nextlimiter#readme"
70
- }
70
+ }
package/src/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { Limiter } = require('./core/limiter');
4
4
  const { PRESETS, DEFAULT_PLANS } = require('./core/config');
5
5
  const { MemoryStore } = require('./store/memoryStore');
6
+ const { RedisStore } = require('./store/redisStore');
6
7
 
7
8
  /**
8
9
  * Create a fully configured rate limiter instance.
@@ -99,6 +100,7 @@ module.exports = {
99
100
 
100
101
  // Storage
101
102
  MemoryStore,
103
+ RedisStore,
102
104
 
103
105
  // Constants
104
106
  PRESETS,
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * RedisStore — Redis-backed storage backend for NextLimiter.
5
+ *
6
+ * Requires ioredis to be installed separately:
7
+ * npm install ioredis
8
+ *
9
+ * Uses a Lua script for atomic increment so it is safe across multiple servers
10
+ * behind a load balancer — no race conditions, no split-brain issues.
11
+ *
12
+ * Implements the NextLimiter Store interface:
13
+ * get(key) → Promise<value | undefined>
14
+ * set(key, value, ttlMs) → Promise<void>
15
+ * increment(key, ttlMs) → Promise<number> (atomic via Lua)
16
+ * delete(key) → Promise<void>
17
+ * keys() → [] (SCAN not required by the interface)
18
+ */
19
+
20
+ /**
21
+ * Lua script for atomic rate-limit increment.
22
+ *
23
+ * KEYS[1] — the rate limit key
24
+ * ARGV[1] — the max limit (not used for blocking in this script; handled in JS)
25
+ * ARGV[2] — TTL in milliseconds (applied only on first request in window)
26
+ *
27
+ * Logic:
28
+ * 1. GET current value (default 0 if nil)
29
+ * 2. INCR the key
30
+ * 3. If new value == 1 (first request), set PEXPIRE to establish the window TTL
31
+ * 4. Return the new count
32
+ *
33
+ * Atomicity guarantee: all steps execute as a single Redis transaction — no
34
+ * other command can interleave between the GET, INCR, and PEXPIRE.
35
+ */
36
+ const INCR_SCRIPT = `
37
+ local current = tonumber(redis.call('GET', KEYS[1])) or 0
38
+ local new = redis.call('INCR', KEYS[1])
39
+ if new == 1 then
40
+ redis.call('PEXPIRE', KEYS[1], tonumber(ARGV[1]))
41
+ end
42
+ return new
43
+ `;
44
+
45
+ class RedisStore {
46
+ /**
47
+ * @param {import('ioredis').Redis} client — a connected ioredis client instance
48
+ */
49
+ constructor(client) {
50
+ if (!client || typeof client.eval !== 'function') {
51
+ throw new Error(
52
+ '[NextLimiter] RedisStore requires an ioredis client instance. ' +
53
+ 'Install it with: npm install ioredis'
54
+ );
55
+ }
56
+ this._client = client;
57
+ }
58
+
59
+ // ── Store interface ─────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Get a stored value by key.
63
+ * @param {string} key
64
+ * @returns {Promise<any>}
65
+ */
66
+ async get(key) {
67
+ const raw = await this._client.get(key);
68
+ if (raw === null || raw === undefined) return undefined;
69
+ try {
70
+ return JSON.parse(raw);
71
+ } catch {
72
+ return raw;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Set a value with a TTL.
78
+ * @param {string} key
79
+ * @param {any} value
80
+ * @param {number} ttlMs - Time to live in milliseconds
81
+ */
82
+ async set(key, value, ttlMs) {
83
+ await this._client.set(key, JSON.stringify(value), 'PX', ttlMs);
84
+ }
85
+
86
+ /**
87
+ * Atomically increment the request counter for a key using a Lua script.
88
+ * Sets the window TTL on first request. Returns the new count.
89
+ *
90
+ * @param {string} key
91
+ * @param {number} ttlMs - Window TTL in milliseconds (set on first request only)
92
+ * @returns {Promise<number>} new count after increment
93
+ */
94
+ async increment(key, ttlMs) {
95
+ const result = await this._client.eval(
96
+ INCR_SCRIPT,
97
+ 1, // number of KEYS
98
+ key, // KEYS[1]
99
+ ttlMs // ARGV[1] — TTL in ms
100
+ );
101
+ return Number(result);
102
+ }
103
+
104
+ /**
105
+ * Delete a key.
106
+ * @param {string} key
107
+ */
108
+ async delete(key) {
109
+ await this._client.del(key);
110
+ }
111
+
112
+ /**
113
+ * Return tracked keys. Redis SCAN is optional per the Store interface,
114
+ * so we return an empty array here. Use Redis SCAN / HSCAN externally
115
+ * if you need to inspect active keys in production.
116
+ * @returns {string[]}
117
+ */
118
+ keys() {
119
+ return [];
120
+ }
121
+
122
+ /** No-op — Redis keys expire automatically via PEXPIRE. */
123
+ clear() {}
124
+ }
125
+
126
+ module.exports = { RedisStore };
127
+
128
+ // ── Usage example ─────────────────────────────────────────────────────────────
129
+ //
130
+ // const Redis = require('ioredis');
131
+ // const { createLimiter, RedisStore } = require('nextlimiter');
132
+ //
133
+ // const redis = new Redis(); // connects to 127.0.0.1:6379 by default
134
+ //
135
+ // const limiter = createLimiter({
136
+ // store: new RedisStore(redis),
137
+ // max: 100,
138
+ // windowMs: 60_000,
139
+ // strategy: 'sliding-window',
140
+ // keyBy: 'ip',
141
+ // logging: true,
142
+ // });
143
+ //
144
+ // app.use('/api', limiter.middleware());
145
+ //
146
+ // // Programmatic check (WebSockets, background jobs, etc.)
147
+ // const result = await limiter.check(`user:${userId}`);
148
+ // if (!result.allowed) throw new Error(`Rate limited. Retry in ${result.retryAfter}s`);
149
+ //
150
+ // ─────────────────────────────────────────────────────────────────────────────
package/types/index.d.ts CHANGED
@@ -240,6 +240,37 @@ export declare class MemoryStore implements Store {
240
240
  readonly size: number;
241
241
  }
242
242
 
243
+ /**
244
+ * RedisStore — Redis-backed storage backend for NextLimiter.
245
+ *
246
+ * Requires ioredis to be installed separately:
247
+ * npm install ioredis
248
+ *
249
+ * Uses an atomic Lua script for increment — race-condition safe across
250
+ * multiple Node.js processes behind a load balancer.
251
+ *
252
+ * @example
253
+ * import Redis from 'ioredis';
254
+ * import { createLimiter, RedisStore } from 'nextlimiter';
255
+ *
256
+ * const redis = new Redis();
257
+ * const limiter = createLimiter({ store: new RedisStore(redis), max: 100 });
258
+ * app.use('/api', limiter.middleware());
259
+ */
260
+ export declare class RedisStore implements Store {
261
+ /**
262
+ * @param client - A connected ioredis client instance
263
+ */
264
+ constructor(client: any);
265
+ get(key: string): Promise<any>;
266
+ set(key: string, value: any, ttlMs: number): Promise<void>;
267
+ /** Atomically increments the counter using a Lua script. */
268
+ increment(key: string, ttlMs: number): Promise<number>;
269
+ delete(key: string): Promise<void>;
270
+ keys(): string[];
271
+ clear(): void;
272
+ }
273
+
243
274
  // ── Constants ────────────────────────────────────────────────────────────────
244
275
 
245
276
  export declare const PRESETS: Record<BuiltInPreset, LimiterOptions>;