pg-ratelimit 0.1.0 → 0.2.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/dist/index.cjs +36 -0
- package/dist/index.d.cts +18 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +36 -0
- package/package.json +20 -20
- package/LICENSE +0 -21
package/dist/index.cjs
CHANGED
|
@@ -276,6 +276,10 @@ var Ratelimit = class {
|
|
|
276
276
|
durable;
|
|
277
277
|
synchronousCommit;
|
|
278
278
|
cleanupProbability;
|
|
279
|
+
inMemoryBlock;
|
|
280
|
+
maxBlockedKeys;
|
|
281
|
+
limitValue;
|
|
282
|
+
blockedKeys;
|
|
279
283
|
// Pre-parsed durations (only the relevant one is used per algorithm)
|
|
280
284
|
windowMs;
|
|
281
285
|
intervalMs;
|
|
@@ -303,6 +307,10 @@ var Ratelimit = class {
|
|
|
303
307
|
this.windowMs = 0;
|
|
304
308
|
this.intervalMs = toMs(this.algorithm.interval);
|
|
305
309
|
}
|
|
310
|
+
this.inMemoryBlock = "inMemoryBlock" in config && config.inMemoryBlock === true;
|
|
311
|
+
this.maxBlockedKeys = this.inMemoryBlock && "maxBlockedKeys" in config ? config.maxBlockedKeys ?? 1e4 : 1e4;
|
|
312
|
+
this.blockedKeys = /* @__PURE__ */ new Map();
|
|
313
|
+
this.limitValue = this.algorithm.type === "tokenBucket" ? this.algorithm.maxTokens : this.algorithm.tokens;
|
|
306
314
|
}
|
|
307
315
|
static fixedWindow(tokens, window) {
|
|
308
316
|
return { type: "fixedWindow", tokens, window };
|
|
@@ -316,6 +324,16 @@ var Ratelimit = class {
|
|
|
316
324
|
async limit(key, opts) {
|
|
317
325
|
const rate = opts?.rate ?? 1;
|
|
318
326
|
const now = this.clock();
|
|
327
|
+
const nowMs = now.getTime();
|
|
328
|
+
if (this.inMemoryBlock && rate > 0) {
|
|
329
|
+
const cachedReset = this.blockedKeys.get(key);
|
|
330
|
+
if (cachedReset !== void 0) {
|
|
331
|
+
if (cachedReset > nowMs) {
|
|
332
|
+
return { success: false, limit: this.limitValue, remaining: 0, reset: cachedReset };
|
|
333
|
+
}
|
|
334
|
+
this.blockedKeys.delete(key);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
319
337
|
await ensureTables(this.pool);
|
|
320
338
|
const ctx = {
|
|
321
339
|
pool: this.pool,
|
|
@@ -344,6 +362,16 @@ var Ratelimit = class {
|
|
|
344
362
|
);
|
|
345
363
|
break;
|
|
346
364
|
}
|
|
365
|
+
if (this.inMemoryBlock) {
|
|
366
|
+
if (rate < 0) {
|
|
367
|
+
this.blockedKeys.delete(key);
|
|
368
|
+
} else if (!result.success) {
|
|
369
|
+
this.blockedKeys.set(key, result.reset);
|
|
370
|
+
if (this.blockedKeys.size > this.maxBlockedKeys) {
|
|
371
|
+
this.sweepExpired(nowMs);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
347
375
|
if (Math.random() < this.cleanupProbability) {
|
|
348
376
|
void this.pool.query(`DELETE FROM ${this.table} WHERE prefix = $1 AND expires_at < $2`, [
|
|
349
377
|
this.prefix,
|
|
@@ -473,6 +501,14 @@ var Ratelimit = class {
|
|
|
473
501
|
console.debug("pg-ratelimit resetUsedTokens:", sql, [this.prefix, key]);
|
|
474
502
|
}
|
|
475
503
|
await this.pool.query(sql, [this.prefix, key]);
|
|
504
|
+
this.blockedKeys.delete(key);
|
|
505
|
+
}
|
|
506
|
+
sweepExpired(nowMs) {
|
|
507
|
+
for (const [k, reset] of this.blockedKeys) {
|
|
508
|
+
if (reset <= nowMs) {
|
|
509
|
+
this.blockedKeys.delete(k);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
476
512
|
}
|
|
477
513
|
};
|
|
478
514
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.d.cts
CHANGED
|
@@ -23,6 +23,18 @@ interface LimitResult {
|
|
|
23
23
|
reset: number;
|
|
24
24
|
}
|
|
25
25
|
type Clock = () => Date;
|
|
26
|
+
type InMemoryBlockConfig = {
|
|
27
|
+
inMemoryBlock?: false;
|
|
28
|
+
} | {
|
|
29
|
+
inMemoryBlock: true;
|
|
30
|
+
maxBlockedKeys?: number;
|
|
31
|
+
};
|
|
32
|
+
type DurableConfig = {
|
|
33
|
+
durable?: false;
|
|
34
|
+
} | {
|
|
35
|
+
durable: true;
|
|
36
|
+
synchronousCommit?: boolean;
|
|
37
|
+
};
|
|
26
38
|
type RatelimitConfig = {
|
|
27
39
|
pool: Pool;
|
|
28
40
|
limiter: Algorithm;
|
|
@@ -30,12 +42,7 @@ type RatelimitConfig = {
|
|
|
30
42
|
debug?: boolean;
|
|
31
43
|
clock?: Clock;
|
|
32
44
|
cleanupProbability?: number;
|
|
33
|
-
} &
|
|
34
|
-
durable?: false;
|
|
35
|
-
} | {
|
|
36
|
-
durable: true;
|
|
37
|
-
synchronousCommit?: boolean;
|
|
38
|
-
});
|
|
45
|
+
} & InMemoryBlockConfig & DurableConfig;
|
|
39
46
|
|
|
40
47
|
declare class Ratelimit {
|
|
41
48
|
private readonly pool;
|
|
@@ -47,6 +54,10 @@ declare class Ratelimit {
|
|
|
47
54
|
private readonly durable;
|
|
48
55
|
private readonly synchronousCommit;
|
|
49
56
|
private readonly cleanupProbability;
|
|
57
|
+
private readonly inMemoryBlock;
|
|
58
|
+
private readonly maxBlockedKeys;
|
|
59
|
+
private readonly limitValue;
|
|
60
|
+
private readonly blockedKeys;
|
|
50
61
|
private readonly windowMs;
|
|
51
62
|
private readonly intervalMs;
|
|
52
63
|
constructor(config: RatelimitConfig);
|
|
@@ -64,6 +75,7 @@ declare class Ratelimit {
|
|
|
64
75
|
reset: number;
|
|
65
76
|
}>;
|
|
66
77
|
resetUsedTokens(key: string): Promise<void>;
|
|
78
|
+
private sweepExpired;
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
declare const TABLE_SQL: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,18 @@ interface LimitResult {
|
|
|
23
23
|
reset: number;
|
|
24
24
|
}
|
|
25
25
|
type Clock = () => Date;
|
|
26
|
+
type InMemoryBlockConfig = {
|
|
27
|
+
inMemoryBlock?: false;
|
|
28
|
+
} | {
|
|
29
|
+
inMemoryBlock: true;
|
|
30
|
+
maxBlockedKeys?: number;
|
|
31
|
+
};
|
|
32
|
+
type DurableConfig = {
|
|
33
|
+
durable?: false;
|
|
34
|
+
} | {
|
|
35
|
+
durable: true;
|
|
36
|
+
synchronousCommit?: boolean;
|
|
37
|
+
};
|
|
26
38
|
type RatelimitConfig = {
|
|
27
39
|
pool: Pool;
|
|
28
40
|
limiter: Algorithm;
|
|
@@ -30,12 +42,7 @@ type RatelimitConfig = {
|
|
|
30
42
|
debug?: boolean;
|
|
31
43
|
clock?: Clock;
|
|
32
44
|
cleanupProbability?: number;
|
|
33
|
-
} &
|
|
34
|
-
durable?: false;
|
|
35
|
-
} | {
|
|
36
|
-
durable: true;
|
|
37
|
-
synchronousCommit?: boolean;
|
|
38
|
-
});
|
|
45
|
+
} & InMemoryBlockConfig & DurableConfig;
|
|
39
46
|
|
|
40
47
|
declare class Ratelimit {
|
|
41
48
|
private readonly pool;
|
|
@@ -47,6 +54,10 @@ declare class Ratelimit {
|
|
|
47
54
|
private readonly durable;
|
|
48
55
|
private readonly synchronousCommit;
|
|
49
56
|
private readonly cleanupProbability;
|
|
57
|
+
private readonly inMemoryBlock;
|
|
58
|
+
private readonly maxBlockedKeys;
|
|
59
|
+
private readonly limitValue;
|
|
60
|
+
private readonly blockedKeys;
|
|
50
61
|
private readonly windowMs;
|
|
51
62
|
private readonly intervalMs;
|
|
52
63
|
constructor(config: RatelimitConfig);
|
|
@@ -64,6 +75,7 @@ declare class Ratelimit {
|
|
|
64
75
|
reset: number;
|
|
65
76
|
}>;
|
|
66
77
|
resetUsedTokens(key: string): Promise<void>;
|
|
78
|
+
private sweepExpired;
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
declare const TABLE_SQL: string;
|
package/dist/index.js
CHANGED
|
@@ -249,6 +249,10 @@ var Ratelimit = class {
|
|
|
249
249
|
durable;
|
|
250
250
|
synchronousCommit;
|
|
251
251
|
cleanupProbability;
|
|
252
|
+
inMemoryBlock;
|
|
253
|
+
maxBlockedKeys;
|
|
254
|
+
limitValue;
|
|
255
|
+
blockedKeys;
|
|
252
256
|
// Pre-parsed durations (only the relevant one is used per algorithm)
|
|
253
257
|
windowMs;
|
|
254
258
|
intervalMs;
|
|
@@ -276,6 +280,10 @@ var Ratelimit = class {
|
|
|
276
280
|
this.windowMs = 0;
|
|
277
281
|
this.intervalMs = toMs(this.algorithm.interval);
|
|
278
282
|
}
|
|
283
|
+
this.inMemoryBlock = "inMemoryBlock" in config && config.inMemoryBlock === true;
|
|
284
|
+
this.maxBlockedKeys = this.inMemoryBlock && "maxBlockedKeys" in config ? config.maxBlockedKeys ?? 1e4 : 1e4;
|
|
285
|
+
this.blockedKeys = /* @__PURE__ */ new Map();
|
|
286
|
+
this.limitValue = this.algorithm.type === "tokenBucket" ? this.algorithm.maxTokens : this.algorithm.tokens;
|
|
279
287
|
}
|
|
280
288
|
static fixedWindow(tokens, window) {
|
|
281
289
|
return { type: "fixedWindow", tokens, window };
|
|
@@ -289,6 +297,16 @@ var Ratelimit = class {
|
|
|
289
297
|
async limit(key, opts) {
|
|
290
298
|
const rate = opts?.rate ?? 1;
|
|
291
299
|
const now = this.clock();
|
|
300
|
+
const nowMs = now.getTime();
|
|
301
|
+
if (this.inMemoryBlock && rate > 0) {
|
|
302
|
+
const cachedReset = this.blockedKeys.get(key);
|
|
303
|
+
if (cachedReset !== void 0) {
|
|
304
|
+
if (cachedReset > nowMs) {
|
|
305
|
+
return { success: false, limit: this.limitValue, remaining: 0, reset: cachedReset };
|
|
306
|
+
}
|
|
307
|
+
this.blockedKeys.delete(key);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
292
310
|
await ensureTables(this.pool);
|
|
293
311
|
const ctx = {
|
|
294
312
|
pool: this.pool,
|
|
@@ -317,6 +335,16 @@ var Ratelimit = class {
|
|
|
317
335
|
);
|
|
318
336
|
break;
|
|
319
337
|
}
|
|
338
|
+
if (this.inMemoryBlock) {
|
|
339
|
+
if (rate < 0) {
|
|
340
|
+
this.blockedKeys.delete(key);
|
|
341
|
+
} else if (!result.success) {
|
|
342
|
+
this.blockedKeys.set(key, result.reset);
|
|
343
|
+
if (this.blockedKeys.size > this.maxBlockedKeys) {
|
|
344
|
+
this.sweepExpired(nowMs);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
320
348
|
if (Math.random() < this.cleanupProbability) {
|
|
321
349
|
void this.pool.query(`DELETE FROM ${this.table} WHERE prefix = $1 AND expires_at < $2`, [
|
|
322
350
|
this.prefix,
|
|
@@ -446,6 +474,14 @@ var Ratelimit = class {
|
|
|
446
474
|
console.debug("pg-ratelimit resetUsedTokens:", sql, [this.prefix, key]);
|
|
447
475
|
}
|
|
448
476
|
await this.pool.query(sql, [this.prefix, key]);
|
|
477
|
+
this.blockedKeys.delete(key);
|
|
478
|
+
}
|
|
479
|
+
sweepExpired(nowMs) {
|
|
480
|
+
for (const [k, reset] of this.blockedKeys) {
|
|
481
|
+
if (reset <= nowMs) {
|
|
482
|
+
this.blockedKeys.delete(k);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
449
485
|
}
|
|
450
486
|
};
|
|
451
487
|
export {
|
package/package.json
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pg-ratelimit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "PostgreSQL-backed rate limiting for Node.js",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Max Malm",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/benjick/pg-ratelimit.git",
|
|
10
|
-
"directory": "packages/pg-ratelimit"
|
|
11
|
-
},
|
|
12
|
-
"homepage": "https://benjick.js.org/pg-ratelimit",
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/benjick/pg-ratelimit/issues"
|
|
15
|
-
},
|
|
16
5
|
"keywords": [
|
|
17
|
-
"rate-limit",
|
|
18
|
-
"ratelimit",
|
|
19
6
|
"postgres",
|
|
20
7
|
"postgresql",
|
|
8
|
+
"rate-limit",
|
|
21
9
|
"rate-limiting",
|
|
10
|
+
"ratelimit",
|
|
22
11
|
"throttle"
|
|
23
12
|
],
|
|
24
|
-
"
|
|
25
|
-
|
|
13
|
+
"homepage": "https://benjick.js.org/pg-ratelimit",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/benjick/pg-ratelimit/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Max Malm",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/benjick/pg-ratelimit.git",
|
|
22
|
+
"directory": "packages/pg-ratelimit"
|
|
26
23
|
},
|
|
27
24
|
"files": [
|
|
28
25
|
"dist"
|
|
@@ -43,6 +40,10 @@
|
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
42
|
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"test": "vitest run"
|
|
46
|
+
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@testcontainers/postgresql": "^10.0.0",
|
|
48
49
|
"@types/pg": "^8.0.0",
|
|
@@ -54,8 +55,7 @@
|
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"pg": "^8.0.0"
|
|
56
57
|
},
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"test": "vitest run"
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18.0.0"
|
|
60
60
|
}
|
|
61
|
-
}
|
|
61
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Max Malm
|
|
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.
|