@nestjs/throttler
Advanced tools
Comparing version 3.1.0 to 4.0.0
# [2.0.0](https://github.com/nestjs/throttler/compare/v1.2.1...v2.0.0) (2021-07-09) | ||
## 4.0.0 | ||
### Major Changes | ||
- 4803dda: Rewrite the storage service to better handle large numbers of operations | ||
## Why | ||
The initial behavior was that `getRecord()` returned an list of sorted TTL | ||
timestamps, then if it didn't reach the limit, it will call `addRecord()`. | ||
This change was made based on the use of the Redis storage community package | ||
where it was found how to prevent this issue. It was found out that | ||
[express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | ||
is incrementing a single number and returning the information in a single | ||
roundtrip, which is significantly faster than how NestJS throttler works by | ||
called `getRecord()`, then `addRecord`. | ||
## Breaking Changes | ||
- removed `getRecord` | ||
- `addRecord(key: string, ttl: number): Promise<number[]>;` changes to `increment(key: string, ttl: number): Promise<ThrottlerStorageRecord>;` | ||
## How to Migrate | ||
If you are just _using_ the throttler library, you're already covered. No | ||
changes necessary to your code, version 4.0.0 will work as is. | ||
If you are providing a custom storage, you will need to remove your current | ||
service's `getRecord` method and rename `addRecord` to `incremenet` while | ||
adhering to the new interface and returning an `ThrottlerStorageRecord` object | ||
## 3.1.0 | ||
@@ -4,0 +35,0 @@ |
@@ -0,6 +1,7 @@ | ||
import { ThrottlerStorageOptions } from './throttler-storage-options.interface'; | ||
import { ThrottlerStorageRecord } from './throttler-storage-record.interface'; | ||
export interface ThrottlerStorage { | ||
storage: Record<string, number[]>; | ||
getRecord(key: string): Promise<number[]>; | ||
addRecord(key: string, ttl: number): Promise<void>; | ||
storage: Record<string, ThrottlerStorageOptions>; | ||
increment(key: string, ttl: number): Promise<ThrottlerStorageRecord>; | ||
} | ||
export declare const ThrottlerStorage: unique symbol; |
@@ -62,12 +62,10 @@ "use strict"; | ||
const key = this.generateKey(context, tracker); | ||
const ttls = await this.storageService.getRecord(key); | ||
const nearestExpiryTime = ttls.length > 0 ? Math.ceil((ttls[0] - Date.now()) / 1000) : 0; | ||
if (ttls.length >= limit) { | ||
res.header('Retry-After', nearestExpiryTime); | ||
const { totalHits, timeToExpire } = await this.storageService.increment(key, ttl); | ||
if (totalHits > limit) { | ||
res.header('Retry-After', timeToExpire); | ||
this.throwThrottlingException(context); | ||
} | ||
res.header(`${this.headerPrefix}-Limit`, limit); | ||
res.header(`${this.headerPrefix}-Remaining`, Math.max(0, limit - (ttls.length + 1))); | ||
res.header(`${this.headerPrefix}-Reset`, nearestExpiryTime); | ||
await this.storageService.addRecord(key, ttl); | ||
res.header(`${this.headerPrefix}-Remaining`, Math.max(0, limit - totalHits)); | ||
res.header(`${this.headerPrefix}-Reset`, timeToExpire); | ||
return true; | ||
@@ -74,0 +72,0 @@ } |
import { OnApplicationShutdown } from '@nestjs/common'; | ||
import { ThrottlerStorageOptions } from './throttler-storage-options.interface'; | ||
import { ThrottlerStorageRecord } from './throttler-storage-record.interface'; | ||
import { ThrottlerStorage } from './throttler-storage.interface'; | ||
@@ -6,6 +8,7 @@ export declare class ThrottlerStorageService implements ThrottlerStorage, OnApplicationShutdown { | ||
private timeoutIds; | ||
get storage(): Record<string, number[]>; | ||
getRecord(key: string): Promise<number[]>; | ||
addRecord(key: string, ttl: number): Promise<void>; | ||
get storage(): Record<string, ThrottlerStorageOptions>; | ||
private getExpirationTime; | ||
private setExpirationTime; | ||
increment(key: string, ttl: number): Promise<ThrottlerStorageRecord>; | ||
onApplicationShutdown(): void; | ||
} |
@@ -19,13 +19,8 @@ "use strict"; | ||
} | ||
async getRecord(key) { | ||
return this.storage[key] || []; | ||
getExpirationTime(key) { | ||
return Math.floor((this.storage[key].expiresAt - Date.now()) / 1000); | ||
} | ||
async addRecord(key, ttl) { | ||
const ttlMilliseconds = ttl * 1000; | ||
if (!this.storage[key]) { | ||
this.storage[key] = []; | ||
} | ||
this.storage[key].push(Date.now() + ttlMilliseconds); | ||
setExpirationTime(key, ttlMilliseconds) { | ||
const timeoutId = setTimeout(() => { | ||
this.storage[key].shift(); | ||
this.storage[key].totalHits--; | ||
clearTimeout(timeoutId); | ||
@@ -36,2 +31,19 @@ this.timeoutIds = this.timeoutIds.filter((id) => id != timeoutId); | ||
} | ||
async increment(key, ttl) { | ||
const ttlMilliseconds = ttl * 1000; | ||
if (!this.storage[key]) { | ||
this.storage[key] = { totalHits: 0, expiresAt: Date.now() + ttlMilliseconds }; | ||
} | ||
let timeToExpire = this.getExpirationTime(key); | ||
if (timeToExpire <= 0) { | ||
this.storage[key].expiresAt = Date.now() + ttlMilliseconds; | ||
timeToExpire = this.getExpirationTime(key); | ||
} | ||
this.storage[key].totalHits++; | ||
this.setExpirationTime(key, ttlMilliseconds); | ||
return { | ||
totalHits: this.storage[key].totalHits, | ||
timeToExpire, | ||
}; | ||
} | ||
onApplicationShutdown() { | ||
@@ -38,0 +50,0 @@ this.timeoutIds.forEach(clearTimeout); |
{ | ||
"name": "@nestjs/throttler", | ||
"version": "3.1.0", | ||
"version": "4.0.0", | ||
"description": "A Rate-Limiting module for NestJS to work on Express, Fastify, Websockets, Socket.IO, and GraphQL, all rolled up into a simple package.", | ||
@@ -51,51 +51,51 @@ "author": "Jay McDoniel <me@jaymcdoniel.dev>", | ||
"devDependencies": { | ||
"@changesets/cli": "2.25.0", | ||
"@commitlint/cli": "17.1.2", | ||
"@commitlint/config-angular": "17.1.0", | ||
"@nestjs/cli": "9.1.4", | ||
"@nestjs/common": "9.1.3", | ||
"@nestjs/core": "9.1.3", | ||
"@nestjs/graphql": "10.1.3", | ||
"@nestjs/platform-express": "9.1.3", | ||
"@nestjs/platform-fastify": "9.1.3", | ||
"@nestjs/platform-socket.io": "9.1.3", | ||
"@nestjs/platform-ws": "9.1.3", | ||
"@nestjs/schematics": "9.0.3", | ||
"@nestjs/testing": "9.1.3", | ||
"@nestjs/websockets": "9.1.3", | ||
"@changesets/cli": "2.26.0", | ||
"@commitlint/cli": "17.4.2", | ||
"@commitlint/config-angular": "17.4.2", | ||
"@nestjs/cli": "9.1.8", | ||
"@nestjs/common": "9.2.1", | ||
"@nestjs/core": "9.2.1", | ||
"@nestjs/graphql": "10.1.7", | ||
"@nestjs/platform-express": "9.2.1", | ||
"@nestjs/platform-fastify": "9.2.1", | ||
"@nestjs/platform-socket.io": "9.2.1", | ||
"@nestjs/platform-ws": "9.2.1", | ||
"@nestjs/schematics": "9.0.4", | ||
"@nestjs/testing": "9.2.1", | ||
"@nestjs/websockets": "9.2.1", | ||
"@semantic-release/git": "10.0.1", | ||
"@types/express": "4.17.14", | ||
"@types/express-serve-static-core": "4.17.31", | ||
"@types/jest": "29.1.2", | ||
"@types/express": "4.17.15", | ||
"@types/express-serve-static-core": "4.17.32", | ||
"@types/jest": "29.2.6", | ||
"@types/md5": "2.3.2", | ||
"@types/node": "16.11.65", | ||
"@types/node": "18.11.18", | ||
"@types/supertest": "2.0.12", | ||
"@typescript-eslint/eslint-plugin": "5.40.0", | ||
"@typescript-eslint/parser": "5.40.0", | ||
"apollo-server-express": "3.10.3", | ||
"apollo-server-fastify": "3.10.3", | ||
"@typescript-eslint/eslint-plugin": "5.48.2", | ||
"@typescript-eslint/parser": "5.48.2", | ||
"apollo-server-express": "3.11.1", | ||
"apollo-server-fastify": "3.11.1", | ||
"conventional-changelog-cli": "2.2.2", | ||
"cz-conventional-changelog": "3.3.0", | ||
"eslint": "8.25.0", | ||
"eslint-config-prettier": "8.5.0", | ||
"eslint-plugin-import": "2.26.0", | ||
"eslint": "8.32.0", | ||
"eslint-config-prettier": "8.6.0", | ||
"eslint-plugin-import": "2.27.5", | ||
"graphql": "16.6.0", | ||
"graphql-tools": "8.3.6", | ||
"husky": "8.0.1", | ||
"jest": "29.1.2", | ||
"lint-staged": "13.0.3", | ||
"graphql-tools": "8.3.15", | ||
"husky": "8.0.3", | ||
"jest": "29.3.1", | ||
"lint-staged": "13.1.0", | ||
"nodemon": "2.0.20", | ||
"pinst": "3.0.0", | ||
"prettier": "2.7.1", | ||
"prettier": "2.8.3", | ||
"reflect-metadata": "0.1.13", | ||
"rimraf": "3.0.2", | ||
"rxjs": "7.5.7", | ||
"socket.io": "4.5.2", | ||
"supertest": "6.3.0", | ||
"ts-jest": "29.0.3", | ||
"ts-loader": "9.4.1", | ||
"rimraf": "4.1.1", | ||
"rxjs": "7.8.0", | ||
"socket.io": "4.5.4", | ||
"supertest": "6.3.3", | ||
"ts-jest": "29.0.5", | ||
"ts-loader": "9.4.2", | ||
"ts-node": "10.9.1", | ||
"tsconfig-paths": "4.1.0", | ||
"typescript": "4.8.4", | ||
"ws": "8.9.0" | ||
"tsconfig-paths": "4.1.2", | ||
"typescript": "4.9.4", | ||
"ws": "8.12.0" | ||
}, | ||
@@ -102,0 +102,0 @@ "peerDependencies": { |
@@ -34,3 +34,2 @@ <p align="center"> | ||
## Installation | ||
@@ -220,4 +219,4 @@ | ||
export interface ThrottlerStorage { | ||
getRecord(key: string): Promise<number[]>; | ||
addRecord(key: string, ttl: number): Promise<void>; | ||
storage: Record<string, ThrottlerStorageOptions>; | ||
increment(key: string, ttl: number): Promise<ThrottlerStorageRecord>; | ||
} | ||
@@ -264,9 +263,8 @@ ``` | ||
const key = this.generateKey(context, ip); | ||
const ttls = await this.storageService.getRecord(key); | ||
const { totalHits } = await this.storageService.increment(key, ttl); | ||
if (ttls.length >= limit) { | ||
if (totalHits > limit) { | ||
throw new ThrottlerException(); | ||
} | ||
await this.storageService.addRecord(key, ttl); | ||
return true; | ||
@@ -312,2 +310,2 @@ } | ||
Nest is [MIT licensed](LICENSE). | ||
Nest is [MIT licensed](LICENSE). |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
148856
41
438
308