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 +2 -2
- package/src/index.js +2 -0
- package/src/store/redisStore.js +150 -0
- package/types/index.d.ts +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextlimiter",
|
|
3
|
-
"version": "1.0.
|
|
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>;
|