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 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.1.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
- "engines": {
25
- "node": ">=18.0.0"
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
- "scripts": {
58
- "build": "tsup",
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.