io-ratelimiter 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.
- package/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +208 -0
- package/dist/index.mjs +204 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Devhuset
|
|
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,292 @@
|
|
|
1
|
+
# io-ratelimiter
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/io-ratelimiter)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A flexible Redis-based rate limiting library supporting both fixed and sliding window algorithms. Works with **any Redis client** (ioredis, iovalkey, node-redis, Bun's native client, etc.).
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Fixed window** rate limiting
|
|
11
|
+
- **Sliding window** rate limiting with weighted scoring
|
|
12
|
+
- Redis/Valkey-backed for distributed systems
|
|
13
|
+
- Full TypeScript support
|
|
14
|
+
- High performance Lua script execution (EVALSHA)
|
|
15
|
+
- Protection against race conditions
|
|
16
|
+
- **Client-agnostic** - bring your own Redis client
|
|
17
|
+
- Zero runtime dependencies
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun add io-ratelimiter
|
|
23
|
+
# or
|
|
24
|
+
npm install io-ratelimiter
|
|
25
|
+
# or
|
|
26
|
+
yarn add io-ratelimiter
|
|
27
|
+
# or
|
|
28
|
+
pnpm add io-ratelimiter
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You'll also need a Redis client:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Choose one:
|
|
35
|
+
npm install ioredis
|
|
36
|
+
npm install iovalkey
|
|
37
|
+
npm install redis # node-redis
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { Ratelimit } from 'io-ratelimiter';
|
|
44
|
+
import Redis from 'iovalkey'; // or 'ioredis' or 'redis'
|
|
45
|
+
|
|
46
|
+
// Create any Redis client
|
|
47
|
+
const client = new Redis('redis://localhost:6379');
|
|
48
|
+
|
|
49
|
+
// Create rate limiter (10 requests per 60 seconds)
|
|
50
|
+
const limiter = new Ratelimit(
|
|
51
|
+
client,
|
|
52
|
+
Ratelimit.slidingWindow({
|
|
53
|
+
limit: 10,
|
|
54
|
+
window: 60, // seconds
|
|
55
|
+
prefix: 'my-api', // optional
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Check rate limit
|
|
60
|
+
const result = await limiter.limit('user-123');
|
|
61
|
+
if (result.success) {
|
|
62
|
+
// Process request
|
|
63
|
+
console.log(`${result.remaining} requests remaining`);
|
|
64
|
+
} else {
|
|
65
|
+
// Rate limit exceeded
|
|
66
|
+
console.log(`Try again in ${result.retry_after}ms`);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Rate Limiting Algorithms
|
|
71
|
+
|
|
72
|
+
### Fixed Window
|
|
73
|
+
|
|
74
|
+
Divides time into fixed intervals (e.g., 60-second windows) and tracks requests within each window. Simple and fast.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const limiter = new Ratelimit(
|
|
78
|
+
client,
|
|
79
|
+
Ratelimit.fixedWindow({
|
|
80
|
+
limit: 100,
|
|
81
|
+
window: 60,
|
|
82
|
+
prefix: 'api',
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Best for**: High-throughput scenarios where slight bursts are acceptable
|
|
88
|
+
|
|
89
|
+
### Sliding Window
|
|
90
|
+
|
|
91
|
+
Provides smoother rate limiting by considering both current and previous windows with weighted rates. More accurate but requires Lua script support.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const limiter = new Ratelimit(
|
|
95
|
+
client,
|
|
96
|
+
Ratelimit.slidingWindow({
|
|
97
|
+
limit: 100,
|
|
98
|
+
window: 60,
|
|
99
|
+
prefix: 'api',
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Best for**: Preventing burst traffic, fair rate limiting
|
|
105
|
+
|
|
106
|
+
**Note**: Requires `script()` and `evalsha()` support. Works with ioredis, iovalkey, and node-redis. Bun's native Redis client currently only supports fixed window.
|
|
107
|
+
|
|
108
|
+
## Client Compatibility
|
|
109
|
+
|
|
110
|
+
| Client | Fixed Window | Sliding Window |
|
|
111
|
+
| ------------------------------------------------- | ------------ | -------------- |
|
|
112
|
+
| [ioredis](https://github.com/redis/ioredis) | ✅ | ✅ |
|
|
113
|
+
| [iovalkey](https://github.com/valkey-io/iovalkey) | ✅ | ✅ |
|
|
114
|
+
| [node-redis](https://github.com/redis/node-redis) | ✅ | ✅ |
|
|
115
|
+
| [Bun Redis](https://bun.sh/docs/runtime/redis) | ✅ | ❌ |
|
|
116
|
+
|
|
117
|
+
## API Reference
|
|
118
|
+
|
|
119
|
+
### `Ratelimit` Class
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { Ratelimit, type RedisClient, type RatelimitResponse } from 'io-ratelimiter';
|
|
123
|
+
|
|
124
|
+
const limiter = new Ratelimit(client: RedisClient, options: RatelimitOptions);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Configuration Options
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
interface RatelimitOptionsWithoutType {
|
|
131
|
+
/** Maximum requests per window */
|
|
132
|
+
limit: number;
|
|
133
|
+
/** Window duration in seconds */
|
|
134
|
+
window: number;
|
|
135
|
+
/** Optional Redis key prefix */
|
|
136
|
+
prefix?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Factory methods
|
|
140
|
+
Ratelimit.fixedWindow(options: RatelimitOptionsWithoutType)
|
|
141
|
+
Ratelimit.slidingWindow(options: RatelimitOptionsWithoutType)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Response Type
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
interface RatelimitResponse {
|
|
148
|
+
/** Whether the request is allowed */
|
|
149
|
+
success: boolean;
|
|
150
|
+
/** Maximum number of requests allowed in the window */
|
|
151
|
+
limit: number;
|
|
152
|
+
/** Number of remaining requests in current window */
|
|
153
|
+
remaining: number;
|
|
154
|
+
/** Time in milliseconds until the next request will be allowed (0 if under limit) */
|
|
155
|
+
retry_after: number;
|
|
156
|
+
/** Time in milliseconds when the current window expires completely */
|
|
157
|
+
reset: number;
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `limit()` Method
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
await limiter.limit(identifier: string): Promise<RatelimitResponse>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The `identifier` is typically a user ID, IP address, or API key.
|
|
168
|
+
|
|
169
|
+
## Framework Integration Examples
|
|
170
|
+
|
|
171
|
+
### Next.js App Router
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { Ratelimit } from 'io-ratelimiter';
|
|
175
|
+
import { NextResponse } from 'next/server';
|
|
176
|
+
import { headers } from 'next/headers';
|
|
177
|
+
import Redis from 'iovalkey';
|
|
178
|
+
|
|
179
|
+
const client = new Redis(process.env.REDIS_URL);
|
|
180
|
+
const ratelimit = new Ratelimit(
|
|
181
|
+
client,
|
|
182
|
+
Ratelimit.slidingWindow({ limit: 10, window: 60 }),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
export async function GET() {
|
|
186
|
+
const headersList = await headers();
|
|
187
|
+
const ip = headersList.get('x-forwarded-for') || '127.0.0.1';
|
|
188
|
+
|
|
189
|
+
const { success, remaining, reset, retry_after } =
|
|
190
|
+
await ratelimit.limit(ip);
|
|
191
|
+
|
|
192
|
+
if (!success) {
|
|
193
|
+
return NextResponse.json(
|
|
194
|
+
{ error: 'Too many requests' },
|
|
195
|
+
{
|
|
196
|
+
status: 429,
|
|
197
|
+
headers: {
|
|
198
|
+
'X-RateLimit-Limit': '10',
|
|
199
|
+
'X-RateLimit-Remaining': remaining.toString(),
|
|
200
|
+
'X-RateLimit-Reset': reset.toString(),
|
|
201
|
+
'Retry-After': Math.ceil(retry_after / 1000).toString(),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return NextResponse.json({ message: 'Success' });
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Express Middleware
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { Ratelimit } from 'io-ratelimiter';
|
|
215
|
+
import express from 'express';
|
|
216
|
+
import Redis from 'iovalkey';
|
|
217
|
+
|
|
218
|
+
const app = express();
|
|
219
|
+
const client = new Redis(process.env.REDIS_URL);
|
|
220
|
+
const ratelimit = new Ratelimit(
|
|
221
|
+
client,
|
|
222
|
+
Ratelimit.slidingWindow({ limit: 100, window: 60 }),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
app.use(async (req, res, next) => {
|
|
226
|
+
const { success, remaining, reset, retry_after } = await ratelimit.limit(
|
|
227
|
+
req.ip,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
res.setHeader('X-RateLimit-Limit', '100');
|
|
231
|
+
res.setHeader('X-RateLimit-Remaining', remaining.toString());
|
|
232
|
+
res.setHeader('X-RateLimit-Reset', reset.toString());
|
|
233
|
+
|
|
234
|
+
if (!success) {
|
|
235
|
+
res.setHeader('Retry-After', Math.ceil(retry_after / 1000).toString());
|
|
236
|
+
return res.status(429).json({ error: 'Too many requests' });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
next();
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Hono
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { Ratelimit } from 'io-ratelimiter';
|
|
247
|
+
import { Hono } from 'hono';
|
|
248
|
+
import Redis from 'iovalkey';
|
|
249
|
+
|
|
250
|
+
const app = new Hono();
|
|
251
|
+
const client = new Redis(process.env.REDIS_URL);
|
|
252
|
+
const ratelimit = new Ratelimit(
|
|
253
|
+
client,
|
|
254
|
+
Ratelimit.fixedWindow({ limit: 50, window: 60 }),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
app.use('*', async (c, next) => {
|
|
258
|
+
const ip = c.req.header('x-forwarded-for') || 'unknown';
|
|
259
|
+
const { success, remaining, reset, retry_after } =
|
|
260
|
+
await ratelimit.limit(ip);
|
|
261
|
+
|
|
262
|
+
c.header('X-RateLimit-Limit', '50');
|
|
263
|
+
c.header('X-RateLimit-Remaining', remaining.toString());
|
|
264
|
+
|
|
265
|
+
if (!success) {
|
|
266
|
+
return c.json({ error: 'Too many requests' }, 429);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await next();
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Performance
|
|
274
|
+
|
|
275
|
+
The sliding window algorithm uses `SCRIPT LOAD` + `EVALSHA` for optimal performance:
|
|
276
|
+
|
|
277
|
+
- Script is loaded once on first use
|
|
278
|
+
- Subsequent requests send only a 40-byte SHA hash instead of the full ~800-byte Lua script
|
|
279
|
+
- Atomic operations prevent race conditions
|
|
280
|
+
- Automatic retry on script loss (e.g., Redis restart)
|
|
281
|
+
|
|
282
|
+
## Contributing
|
|
283
|
+
|
|
284
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
[MIT](https://choosealicense.com/licenses/mit/)
|
|
289
|
+
|
|
290
|
+
## Credits
|
|
291
|
+
|
|
292
|
+
Built by [Kasper](https://twitter.com/kasperaamodt)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when rate limiter configuration is invalid
|
|
3
|
+
* @example
|
|
4
|
+
* ```ts
|
|
5
|
+
* throw new ConfigurationError('Limit must be greater than 0')
|
|
6
|
+
* ```
|
|
7
|
+
*/
|
|
8
|
+
declare class ConfigurationError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Thrown when rate limit operations fail. Includes the original error if available
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* throw new RatelimitError('Failed to check rate limit', originalError)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
declare class RatelimitError extends Error {
|
|
19
|
+
originalError?: Error | undefined;
|
|
20
|
+
constructor(message: string, originalError?: Error | undefined);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimal Redis client interface required for rate limiting.
|
|
25
|
+
* Compatible with ioredis, iovalkey, node-redis, Bun's native Redis client, and other Redis clients.
|
|
26
|
+
*
|
|
27
|
+
* Note: For sliding window algorithm, `script` and `evalsha` are required.
|
|
28
|
+
* Bun's native Redis client currently doesn't support these, so use ioredis/iovalkey for sliding window.
|
|
29
|
+
*/
|
|
30
|
+
interface RedisClient {
|
|
31
|
+
incr(key: string): Promise<number>;
|
|
32
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
33
|
+
ttl(key: string): Promise<number>;
|
|
34
|
+
script?(...args: unknown[]): Promise<unknown>;
|
|
35
|
+
evalsha?(sha: string, numKeys: number, ...args: (string | number)[]): Promise<unknown>;
|
|
36
|
+
flushdb?(): Promise<unknown>;
|
|
37
|
+
close?(): void;
|
|
38
|
+
quit?(): Promise<unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Response from a rate limit check
|
|
43
|
+
*/
|
|
44
|
+
interface RatelimitResponse {
|
|
45
|
+
/** Whether the request should be allowed */
|
|
46
|
+
success: boolean;
|
|
47
|
+
/** Maximum number of requests allowed in the window */
|
|
48
|
+
limit: number;
|
|
49
|
+
/** Number of remaining requests in current window */
|
|
50
|
+
remaining: number;
|
|
51
|
+
/** Time in milliseconds until the next request will be allowed. 0 if under limit */
|
|
52
|
+
retry_after: number;
|
|
53
|
+
/** Time in milliseconds when the current window expires completely */
|
|
54
|
+
reset: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Base configuration options for both fixed and sliding window rate limiters
|
|
58
|
+
*/
|
|
59
|
+
interface RatelimitOptionsWithoutType {
|
|
60
|
+
/** Maximum number of requests allowed within the window */
|
|
61
|
+
limit: number;
|
|
62
|
+
/** Time window in seconds */
|
|
63
|
+
window: number;
|
|
64
|
+
/** Optional prefix for Valkey keys to prevent collisions */
|
|
65
|
+
prefix?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Complete rate limiter configuration including window type
|
|
69
|
+
*/
|
|
70
|
+
interface RatelimitOptions extends RatelimitOptionsWithoutType {
|
|
71
|
+
/** Type of rate limiting window to use */
|
|
72
|
+
type: 'fixed' | 'sliding';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Redis-based rate limiter supporting both fixed and sliding window algorithms.
|
|
77
|
+
* Fixed window divides time into discrete chunks while sliding window
|
|
78
|
+
* provides smoother rate limiting using weighted scoring.
|
|
79
|
+
*
|
|
80
|
+
* Accepts any Redis-compatible client (ioredis, iovalkey, node-redis, etc.).
|
|
81
|
+
*/
|
|
82
|
+
declare class Ratelimit {
|
|
83
|
+
private readonly options;
|
|
84
|
+
private readonly time_provider;
|
|
85
|
+
private readonly client;
|
|
86
|
+
private scriptSha?;
|
|
87
|
+
constructor(client: RedisClient, options: RatelimitOptions, time_provider?: () => number);
|
|
88
|
+
static fixedWindow(params: RatelimitOptionsWithoutType): RatelimitOptions;
|
|
89
|
+
static slidingWindow(params: RatelimitOptionsWithoutType): RatelimitOptions;
|
|
90
|
+
limit(identifier: string): Promise<RatelimitResponse>;
|
|
91
|
+
private validateOptions;
|
|
92
|
+
private getKey;
|
|
93
|
+
private fixedWindowLimit;
|
|
94
|
+
private slidingWindowLimit;
|
|
95
|
+
private evalSlidingWindow;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { ConfigurationError, Ratelimit, RatelimitError, type RatelimitOptions, type RatelimitOptionsWithoutType, type RatelimitResponse, type RedisClient };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when rate limiter configuration is invalid
|
|
3
|
+
* @example
|
|
4
|
+
* ```ts
|
|
5
|
+
* throw new ConfigurationError('Limit must be greater than 0')
|
|
6
|
+
* ```
|
|
7
|
+
*/
|
|
8
|
+
declare class ConfigurationError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Thrown when rate limit operations fail. Includes the original error if available
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* throw new RatelimitError('Failed to check rate limit', originalError)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
declare class RatelimitError extends Error {
|
|
19
|
+
originalError?: Error | undefined;
|
|
20
|
+
constructor(message: string, originalError?: Error | undefined);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimal Redis client interface required for rate limiting.
|
|
25
|
+
* Compatible with ioredis, iovalkey, node-redis, Bun's native Redis client, and other Redis clients.
|
|
26
|
+
*
|
|
27
|
+
* Note: For sliding window algorithm, `script` and `evalsha` are required.
|
|
28
|
+
* Bun's native Redis client currently doesn't support these, so use ioredis/iovalkey for sliding window.
|
|
29
|
+
*/
|
|
30
|
+
interface RedisClient {
|
|
31
|
+
incr(key: string): Promise<number>;
|
|
32
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
33
|
+
ttl(key: string): Promise<number>;
|
|
34
|
+
script?(...args: unknown[]): Promise<unknown>;
|
|
35
|
+
evalsha?(sha: string, numKeys: number, ...args: (string | number)[]): Promise<unknown>;
|
|
36
|
+
flushdb?(): Promise<unknown>;
|
|
37
|
+
close?(): void;
|
|
38
|
+
quit?(): Promise<unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Response from a rate limit check
|
|
43
|
+
*/
|
|
44
|
+
interface RatelimitResponse {
|
|
45
|
+
/** Whether the request should be allowed */
|
|
46
|
+
success: boolean;
|
|
47
|
+
/** Maximum number of requests allowed in the window */
|
|
48
|
+
limit: number;
|
|
49
|
+
/** Number of remaining requests in current window */
|
|
50
|
+
remaining: number;
|
|
51
|
+
/** Time in milliseconds until the next request will be allowed. 0 if under limit */
|
|
52
|
+
retry_after: number;
|
|
53
|
+
/** Time in milliseconds when the current window expires completely */
|
|
54
|
+
reset: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Base configuration options for both fixed and sliding window rate limiters
|
|
58
|
+
*/
|
|
59
|
+
interface RatelimitOptionsWithoutType {
|
|
60
|
+
/** Maximum number of requests allowed within the window */
|
|
61
|
+
limit: number;
|
|
62
|
+
/** Time window in seconds */
|
|
63
|
+
window: number;
|
|
64
|
+
/** Optional prefix for Valkey keys to prevent collisions */
|
|
65
|
+
prefix?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Complete rate limiter configuration including window type
|
|
69
|
+
*/
|
|
70
|
+
interface RatelimitOptions extends RatelimitOptionsWithoutType {
|
|
71
|
+
/** Type of rate limiting window to use */
|
|
72
|
+
type: 'fixed' | 'sliding';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Redis-based rate limiter supporting both fixed and sliding window algorithms.
|
|
77
|
+
* Fixed window divides time into discrete chunks while sliding window
|
|
78
|
+
* provides smoother rate limiting using weighted scoring.
|
|
79
|
+
*
|
|
80
|
+
* Accepts any Redis-compatible client (ioredis, iovalkey, node-redis, etc.).
|
|
81
|
+
*/
|
|
82
|
+
declare class Ratelimit {
|
|
83
|
+
private readonly options;
|
|
84
|
+
private readonly time_provider;
|
|
85
|
+
private readonly client;
|
|
86
|
+
private scriptSha?;
|
|
87
|
+
constructor(client: RedisClient, options: RatelimitOptions, time_provider?: () => number);
|
|
88
|
+
static fixedWindow(params: RatelimitOptionsWithoutType): RatelimitOptions;
|
|
89
|
+
static slidingWindow(params: RatelimitOptionsWithoutType): RatelimitOptions;
|
|
90
|
+
limit(identifier: string): Promise<RatelimitResponse>;
|
|
91
|
+
private validateOptions;
|
|
92
|
+
private getKey;
|
|
93
|
+
private fixedWindowLimit;
|
|
94
|
+
private slidingWindowLimit;
|
|
95
|
+
private evalSlidingWindow;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { ConfigurationError, Ratelimit, RatelimitError, type RatelimitOptions, type RatelimitOptionsWithoutType, type RatelimitResponse, type RedisClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var ConfigurationError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ConfigurationError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var RatelimitError = class extends Error {
|
|
11
|
+
constructor(message, originalError) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.originalError = originalError;
|
|
14
|
+
this.name = "RatelimitError";
|
|
15
|
+
this.stack = new Error().stack;
|
|
16
|
+
this.message = message;
|
|
17
|
+
this.originalError = originalError;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/client.ts
|
|
22
|
+
var SLIDING_WINDOW_SCRIPT = `
|
|
23
|
+
local current_key = KEYS[1]
|
|
24
|
+
local previous_key = KEYS[2]
|
|
25
|
+
local tokens = tonumber(ARGV[1])
|
|
26
|
+
local now = tonumber(ARGV[2])
|
|
27
|
+
local window = tonumber(ARGV[3])
|
|
28
|
+
local increment_by = tonumber(ARGV[4])
|
|
29
|
+
|
|
30
|
+
local current_count = tonumber(redis.call("GET", current_key) or "0")
|
|
31
|
+
local previous_count = tonumber(redis.call("GET", previous_key) or "0")
|
|
32
|
+
|
|
33
|
+
local time_in_current = now % window
|
|
34
|
+
local time_remaining_previous = window - time_in_current
|
|
35
|
+
local weighted_previous = (previous_count * time_remaining_previous) / window
|
|
36
|
+
local cumulative_count = math.floor(weighted_previous) + current_count + increment_by
|
|
37
|
+
|
|
38
|
+
if cumulative_count > tokens then
|
|
39
|
+
local needed = cumulative_count - tokens + increment_by
|
|
40
|
+
local retry_after = window - time_in_current
|
|
41
|
+
|
|
42
|
+
if previous_count > 0 then
|
|
43
|
+
local time_needed = (needed * window) / previous_count
|
|
44
|
+
retry_after = math.ceil(time_needed)
|
|
45
|
+
|
|
46
|
+
if retry_after > time_remaining_previous then
|
|
47
|
+
retry_after = time_remaining_previous
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
return { -1, retry_after }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
current_count = current_count + increment_by
|
|
55
|
+
redis.call("SET", current_key, current_count)
|
|
56
|
+
redis.call("PEXPIRE", current_key, window * 2 + 1000)
|
|
57
|
+
|
|
58
|
+
return { tokens - (math.floor(weighted_previous) + current_count), 0 }
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
// src/ratelimit.ts
|
|
62
|
+
var Ratelimit = class {
|
|
63
|
+
constructor(client, options, time_provider = Date.now) {
|
|
64
|
+
this.options = options;
|
|
65
|
+
this.time_provider = time_provider;
|
|
66
|
+
this.client = client;
|
|
67
|
+
this.validateOptions(options);
|
|
68
|
+
}
|
|
69
|
+
static fixedWindow(params) {
|
|
70
|
+
return { type: "fixed", ...params };
|
|
71
|
+
}
|
|
72
|
+
static slidingWindow(params) {
|
|
73
|
+
return { type: "sliding", ...params };
|
|
74
|
+
}
|
|
75
|
+
async limit(identifier) {
|
|
76
|
+
try {
|
|
77
|
+
return this.options.type === "fixed" ? await this.fixedWindowLimit(identifier) : await this.slidingWindowLimit(identifier);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof ConfigurationError) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
throw new RatelimitError(
|
|
83
|
+
"Failed to check rate limit",
|
|
84
|
+
error instanceof Error ? error : new Error(String(error))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
validateOptions(options) {
|
|
89
|
+
if (options.limit <= 0) {
|
|
90
|
+
throw new ConfigurationError("Limit must be greater than 0");
|
|
91
|
+
}
|
|
92
|
+
if (options.window <= 0) {
|
|
93
|
+
throw new ConfigurationError("Time window must be greater than 0");
|
|
94
|
+
}
|
|
95
|
+
if (options.type !== "fixed" && options.type !== "sliding") {
|
|
96
|
+
throw new ConfigurationError(
|
|
97
|
+
'Type must be either "fixed" or "sliding"'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
getKey(identifier, suffix) {
|
|
102
|
+
const prefix = this.options.prefix || "ratelimit";
|
|
103
|
+
return `${prefix}:${identifier}:${suffix}`;
|
|
104
|
+
}
|
|
105
|
+
async fixedWindowLimit(identifier) {
|
|
106
|
+
const now = this.time_provider();
|
|
107
|
+
const window_size = this.options.window;
|
|
108
|
+
const current_window = Math.floor(now / (window_size * 1e3));
|
|
109
|
+
const window_key = this.getKey(identifier, current_window.toString());
|
|
110
|
+
const window_end = (current_window + 1) * (window_size * 1e3);
|
|
111
|
+
const count = await this.client.incr(window_key);
|
|
112
|
+
if (count === 1) {
|
|
113
|
+
await this.client.expire(window_key, window_size);
|
|
114
|
+
}
|
|
115
|
+
if (count > this.options.limit) {
|
|
116
|
+
const ttl = await this.client.ttl(window_key);
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
limit: this.options.limit,
|
|
120
|
+
remaining: 0,
|
|
121
|
+
retry_after: Math.max(ttl * 1e3, 0),
|
|
122
|
+
reset: window_end
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
limit: this.options.limit,
|
|
128
|
+
remaining: this.options.limit - count,
|
|
129
|
+
retry_after: 0,
|
|
130
|
+
reset: window_end
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async slidingWindowLimit(identifier) {
|
|
134
|
+
const now = this.time_provider();
|
|
135
|
+
const window = this.options.window * 1e3;
|
|
136
|
+
const current_window = Math.floor(now / window);
|
|
137
|
+
const previous_window = current_window - 1;
|
|
138
|
+
const current_key = this.getKey(identifier, current_window.toString());
|
|
139
|
+
const previous_key = this.getKey(
|
|
140
|
+
identifier,
|
|
141
|
+
previous_window.toString()
|
|
142
|
+
);
|
|
143
|
+
const [remaining, retry_after] = await this.evalSlidingWindow(
|
|
144
|
+
current_key,
|
|
145
|
+
previous_key,
|
|
146
|
+
this.options.limit.toString(),
|
|
147
|
+
now.toString(),
|
|
148
|
+
window.toString(),
|
|
149
|
+
"1"
|
|
150
|
+
);
|
|
151
|
+
return {
|
|
152
|
+
success: remaining >= 0,
|
|
153
|
+
limit: this.options.limit,
|
|
154
|
+
remaining: Math.max(0, remaining),
|
|
155
|
+
retry_after,
|
|
156
|
+
reset: this.time_provider() + this.options.window * 2e3
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async evalSlidingWindow(currentKey, previousKey, limit, now, window, increment) {
|
|
160
|
+
if (!this.client.script || !this.client.evalsha) {
|
|
161
|
+
throw new ConfigurationError(
|
|
162
|
+
"Sliding window algorithm requires a Redis client with script() and evalsha() support for Lua script execution. Please use a client like ioredis, iovalkey, or node-redis."
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (!this.scriptSha) {
|
|
166
|
+
this.scriptSha = await this.client.script(
|
|
167
|
+
"LOAD",
|
|
168
|
+
SLIDING_WINDOW_SCRIPT
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const result = await this.client.evalsha(
|
|
173
|
+
this.scriptSha,
|
|
174
|
+
2,
|
|
175
|
+
currentKey,
|
|
176
|
+
previousKey,
|
|
177
|
+
limit,
|
|
178
|
+
now,
|
|
179
|
+
window,
|
|
180
|
+
increment
|
|
181
|
+
);
|
|
182
|
+
return result;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof Error && error.message.includes("NOSCRIPT")) {
|
|
185
|
+
this.scriptSha = await this.client.script(
|
|
186
|
+
"LOAD",
|
|
187
|
+
SLIDING_WINDOW_SCRIPT
|
|
188
|
+
);
|
|
189
|
+
const result = await this.client.evalsha(
|
|
190
|
+
this.scriptSha,
|
|
191
|
+
2,
|
|
192
|
+
currentKey,
|
|
193
|
+
previousKey,
|
|
194
|
+
limit,
|
|
195
|
+
now,
|
|
196
|
+
window,
|
|
197
|
+
increment
|
|
198
|
+
);
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
exports.ConfigurationError = ConfigurationError;
|
|
207
|
+
exports.Ratelimit = Ratelimit;
|
|
208
|
+
exports.RatelimitError = RatelimitError;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ConfigurationError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ConfigurationError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var RatelimitError = class extends Error {
|
|
9
|
+
constructor(message, originalError) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.originalError = originalError;
|
|
12
|
+
this.name = "RatelimitError";
|
|
13
|
+
this.stack = new Error().stack;
|
|
14
|
+
this.message = message;
|
|
15
|
+
this.originalError = originalError;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/client.ts
|
|
20
|
+
var SLIDING_WINDOW_SCRIPT = `
|
|
21
|
+
local current_key = KEYS[1]
|
|
22
|
+
local previous_key = KEYS[2]
|
|
23
|
+
local tokens = tonumber(ARGV[1])
|
|
24
|
+
local now = tonumber(ARGV[2])
|
|
25
|
+
local window = tonumber(ARGV[3])
|
|
26
|
+
local increment_by = tonumber(ARGV[4])
|
|
27
|
+
|
|
28
|
+
local current_count = tonumber(redis.call("GET", current_key) or "0")
|
|
29
|
+
local previous_count = tonumber(redis.call("GET", previous_key) or "0")
|
|
30
|
+
|
|
31
|
+
local time_in_current = now % window
|
|
32
|
+
local time_remaining_previous = window - time_in_current
|
|
33
|
+
local weighted_previous = (previous_count * time_remaining_previous) / window
|
|
34
|
+
local cumulative_count = math.floor(weighted_previous) + current_count + increment_by
|
|
35
|
+
|
|
36
|
+
if cumulative_count > tokens then
|
|
37
|
+
local needed = cumulative_count - tokens + increment_by
|
|
38
|
+
local retry_after = window - time_in_current
|
|
39
|
+
|
|
40
|
+
if previous_count > 0 then
|
|
41
|
+
local time_needed = (needed * window) / previous_count
|
|
42
|
+
retry_after = math.ceil(time_needed)
|
|
43
|
+
|
|
44
|
+
if retry_after > time_remaining_previous then
|
|
45
|
+
retry_after = time_remaining_previous
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
return { -1, retry_after }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
current_count = current_count + increment_by
|
|
53
|
+
redis.call("SET", current_key, current_count)
|
|
54
|
+
redis.call("PEXPIRE", current_key, window * 2 + 1000)
|
|
55
|
+
|
|
56
|
+
return { tokens - (math.floor(weighted_previous) + current_count), 0 }
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
// src/ratelimit.ts
|
|
60
|
+
var Ratelimit = class {
|
|
61
|
+
constructor(client, options, time_provider = Date.now) {
|
|
62
|
+
this.options = options;
|
|
63
|
+
this.time_provider = time_provider;
|
|
64
|
+
this.client = client;
|
|
65
|
+
this.validateOptions(options);
|
|
66
|
+
}
|
|
67
|
+
static fixedWindow(params) {
|
|
68
|
+
return { type: "fixed", ...params };
|
|
69
|
+
}
|
|
70
|
+
static slidingWindow(params) {
|
|
71
|
+
return { type: "sliding", ...params };
|
|
72
|
+
}
|
|
73
|
+
async limit(identifier) {
|
|
74
|
+
try {
|
|
75
|
+
return this.options.type === "fixed" ? await this.fixedWindowLimit(identifier) : await this.slidingWindowLimit(identifier);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof ConfigurationError) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
throw new RatelimitError(
|
|
81
|
+
"Failed to check rate limit",
|
|
82
|
+
error instanceof Error ? error : new Error(String(error))
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
validateOptions(options) {
|
|
87
|
+
if (options.limit <= 0) {
|
|
88
|
+
throw new ConfigurationError("Limit must be greater than 0");
|
|
89
|
+
}
|
|
90
|
+
if (options.window <= 0) {
|
|
91
|
+
throw new ConfigurationError("Time window must be greater than 0");
|
|
92
|
+
}
|
|
93
|
+
if (options.type !== "fixed" && options.type !== "sliding") {
|
|
94
|
+
throw new ConfigurationError(
|
|
95
|
+
'Type must be either "fixed" or "sliding"'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
getKey(identifier, suffix) {
|
|
100
|
+
const prefix = this.options.prefix || "ratelimit";
|
|
101
|
+
return `${prefix}:${identifier}:${suffix}`;
|
|
102
|
+
}
|
|
103
|
+
async fixedWindowLimit(identifier) {
|
|
104
|
+
const now = this.time_provider();
|
|
105
|
+
const window_size = this.options.window;
|
|
106
|
+
const current_window = Math.floor(now / (window_size * 1e3));
|
|
107
|
+
const window_key = this.getKey(identifier, current_window.toString());
|
|
108
|
+
const window_end = (current_window + 1) * (window_size * 1e3);
|
|
109
|
+
const count = await this.client.incr(window_key);
|
|
110
|
+
if (count === 1) {
|
|
111
|
+
await this.client.expire(window_key, window_size);
|
|
112
|
+
}
|
|
113
|
+
if (count > this.options.limit) {
|
|
114
|
+
const ttl = await this.client.ttl(window_key);
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
limit: this.options.limit,
|
|
118
|
+
remaining: 0,
|
|
119
|
+
retry_after: Math.max(ttl * 1e3, 0),
|
|
120
|
+
reset: window_end
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
limit: this.options.limit,
|
|
126
|
+
remaining: this.options.limit - count,
|
|
127
|
+
retry_after: 0,
|
|
128
|
+
reset: window_end
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async slidingWindowLimit(identifier) {
|
|
132
|
+
const now = this.time_provider();
|
|
133
|
+
const window = this.options.window * 1e3;
|
|
134
|
+
const current_window = Math.floor(now / window);
|
|
135
|
+
const previous_window = current_window - 1;
|
|
136
|
+
const current_key = this.getKey(identifier, current_window.toString());
|
|
137
|
+
const previous_key = this.getKey(
|
|
138
|
+
identifier,
|
|
139
|
+
previous_window.toString()
|
|
140
|
+
);
|
|
141
|
+
const [remaining, retry_after] = await this.evalSlidingWindow(
|
|
142
|
+
current_key,
|
|
143
|
+
previous_key,
|
|
144
|
+
this.options.limit.toString(),
|
|
145
|
+
now.toString(),
|
|
146
|
+
window.toString(),
|
|
147
|
+
"1"
|
|
148
|
+
);
|
|
149
|
+
return {
|
|
150
|
+
success: remaining >= 0,
|
|
151
|
+
limit: this.options.limit,
|
|
152
|
+
remaining: Math.max(0, remaining),
|
|
153
|
+
retry_after,
|
|
154
|
+
reset: this.time_provider() + this.options.window * 2e3
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async evalSlidingWindow(currentKey, previousKey, limit, now, window, increment) {
|
|
158
|
+
if (!this.client.script || !this.client.evalsha) {
|
|
159
|
+
throw new ConfigurationError(
|
|
160
|
+
"Sliding window algorithm requires a Redis client with script() and evalsha() support for Lua script execution. Please use a client like ioredis, iovalkey, or node-redis."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (!this.scriptSha) {
|
|
164
|
+
this.scriptSha = await this.client.script(
|
|
165
|
+
"LOAD",
|
|
166
|
+
SLIDING_WINDOW_SCRIPT
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const result = await this.client.evalsha(
|
|
171
|
+
this.scriptSha,
|
|
172
|
+
2,
|
|
173
|
+
currentKey,
|
|
174
|
+
previousKey,
|
|
175
|
+
limit,
|
|
176
|
+
now,
|
|
177
|
+
window,
|
|
178
|
+
increment
|
|
179
|
+
);
|
|
180
|
+
return result;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error instanceof Error && error.message.includes("NOSCRIPT")) {
|
|
183
|
+
this.scriptSha = await this.client.script(
|
|
184
|
+
"LOAD",
|
|
185
|
+
SLIDING_WINDOW_SCRIPT
|
|
186
|
+
);
|
|
187
|
+
const result = await this.client.evalsha(
|
|
188
|
+
this.scriptSha,
|
|
189
|
+
2,
|
|
190
|
+
currentKey,
|
|
191
|
+
previousKey,
|
|
192
|
+
limit,
|
|
193
|
+
now,
|
|
194
|
+
window,
|
|
195
|
+
increment
|
|
196
|
+
);
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export { ConfigurationError, Ratelimit, RatelimitError };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "io-ratelimiter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "Devhuset",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/kasperaamodt/io-ratelimiter.git"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.mjs",
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "^1.3.5",
|
|
13
|
+
"@types/node": "^25.0.3",
|
|
14
|
+
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
15
|
+
"@typescript-eslint/parser": "^8.50.1",
|
|
16
|
+
"eslint": "^9.39.2",
|
|
17
|
+
"eslint-config-prettier": "^10.1.8",
|
|
18
|
+
"iovalkey": "^0.3.1",
|
|
19
|
+
"prettier": "^3.7.4",
|
|
20
|
+
"tsup": "^8.5.1",
|
|
21
|
+
"typescript": "^5.9.3",
|
|
22
|
+
"typescript-eslint": "^8.50.1"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"require": "./dist/index.js",
|
|
29
|
+
"import": "./dist/index.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/kasperaamodt/io-ratelimiter/issues"
|
|
34
|
+
},
|
|
35
|
+
"description": "A flexible rate limiting library with support for fixed and sliding windows using Redis/Valkey",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"homepage": "https://github.com/kasperaamodt/io-ratelimiter#readme",
|
|
42
|
+
"keywords": [
|
|
43
|
+
"rate-limit",
|
|
44
|
+
"valkey",
|
|
45
|
+
"redis",
|
|
46
|
+
"sliding-window",
|
|
47
|
+
"fixed-window",
|
|
48
|
+
"typescript",
|
|
49
|
+
"rate-limiting"
|
|
50
|
+
],
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"test": "bun test",
|
|
55
|
+
"lint": "eslint 'src/**/*.ts'",
|
|
56
|
+
"format": "prettier --write \"src/**/*.ts\""
|
|
57
|
+
},
|
|
58
|
+
"types": "./dist/index.d.ts"
|
|
59
|
+
}
|