@fluojs/throttler 1.0.2 → 1.0.4
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/README.ko.md +20 -3
- package/README.md +20 -3
- package/dist/guard.d.ts +2 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +39 -21
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/module.d.ts +6 -5
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +6 -5
- package/dist/redis-store.d.ts +25 -2
- package/dist/redis-store.d.ts.map +1 -1
- package/dist/redis-store.js +20 -1
- package/dist/types.d.ts +17 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +19 -1
- package/package.json +7 -7
package/README.ko.md
CHANGED
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
- [공통 패턴](#공통-패턴)
|
|
13
13
|
- [Redis 저장소 사용](#redis-저장소-사용)
|
|
14
14
|
- [커스텀 키 생성](#커스텀-키-생성)
|
|
15
|
+
- [NestJS 마이그레이션 경계](#nestjs-마이그레이션-경계)
|
|
15
16
|
- [공개 API 개요](#공개-api-개요)
|
|
16
17
|
- [관련 패키지](#관련-패키지)
|
|
17
18
|
- [예제 소스](#예제-소스)
|
|
18
19
|
|
|
19
20
|
## 설치
|
|
20
21
|
|
|
22
|
+
`@fluojs/throttler`는 배포 package manifest에서 `engines.node >=20.0.0`을 선언합니다.
|
|
23
|
+
|
|
21
24
|
```bash
|
|
22
25
|
npm install @fluojs/throttler
|
|
23
26
|
```
|
|
@@ -71,6 +74,8 @@ class AuthController {
|
|
|
71
74
|
|
|
72
75
|
다중 인스턴스 배포 환경에서는 `RedisThrottlerStore`를 사용하여 모든 인스턴스 간에 속도 제한 상태를 공유하세요. Redis 기반 윈도우는 Redis 서버 시간을 기준으로 고정되므로, 애플리케이션 노드 간 시계 오차가 있어도 하나의 공통 reset 경계를 강제합니다.
|
|
73
76
|
|
|
77
|
+
`RedisThrottlerStore`는 패키지 로컬 구조적 `RedisThrottlerClient` 계약을 받습니다. 이 계약은 `eval(script, numberOfKeys, ...args)` 메서드를 제공하는 Redis command client를 의미합니다. `@fluojs/redis`, `ioredis`, 호환 custom client를 전달할 수 있으며, root `@fluojs/throttler` import가 concrete `ioredis` constructor type에 의존하지 않습니다.
|
|
78
|
+
|
|
74
79
|
```typescript
|
|
75
80
|
import { ThrottlerModule, RedisThrottlerStore } from '@fluojs/throttler';
|
|
76
81
|
import { REDIS_CLIENT } from '@fluojs/redis';
|
|
@@ -86,7 +91,7 @@ ThrottlerModule.forRoot({
|
|
|
86
91
|
});
|
|
87
92
|
```
|
|
88
93
|
|
|
89
|
-
`ThrottlerStore` 계약을 구현한 객체도 `store` 옵션으로 직접 전달할 수 있습니다.
|
|
94
|
+
`ThrottlerStore` 계약을 구현한 객체도 `store` 옵션으로 직접 전달할 수 있습니다. `ThrottlerModule.forRoot(...)`는 요청 처리 전에 custom store가 `consume(...)` 함수를 노출하는지 검증합니다.
|
|
90
95
|
|
|
91
96
|
### 커스텀 키 생성
|
|
92
97
|
|
|
@@ -119,17 +124,27 @@ ThrottlerModule.forRoot({
|
|
|
119
124
|
});
|
|
120
125
|
```
|
|
121
126
|
|
|
127
|
+
## NestJS 마이그레이션 경계
|
|
128
|
+
|
|
129
|
+
`@nestjs/throttler`에서 마이그레이션할 때 `@fluojs/throttler`는 drop-in 전역 limiter가 아니라 명시적인 guard-stage 패키지로 다뤄야 합니다:
|
|
130
|
+
|
|
131
|
+
- `ThrottlerModule.forRoot(...)`는 검증된 옵션과 provider를 등록하지만, 모든 route에 throttling을 자동으로 강제하지 않습니다. 보호가 필요한 곳마다 `@UseGuards(ThrottlerGuard)` 같은 Fluo guard metadata로 `ThrottlerGuard`를 활성화하세요.
|
|
132
|
+
- 공개 정책 shape는 하나의 module default와 class 또는 method 수준 `@Throttle({ ttl, limit })` override입니다. burst와 sustained limit을 함께 두는 named multi-window definition은 HTTP middleware, custom `ThrottlerStore`, 또는 애플리케이션이 소유한 guard wrapper로 명시적으로 조합해야 합니다.
|
|
133
|
+
- Forwarded client IP header는 기본적으로 무시됩니다. `Forwarded`, `X-Forwarded-For`, `X-Real-IP`를 신뢰 가능한 proxy가 덮어쓰는 배포에서만 `trustProxyHeaders: true`를 활성화하세요.
|
|
134
|
+
- 제한 초과 시 보장되는 응답 계약은 HTTP `429`와 `Retry-After`입니다. 추가 rate-limit header나 response body는 exception filter 같은 애플리케이션 경계에서 더하세요.
|
|
135
|
+
|
|
122
136
|
## 공개 API 개요
|
|
123
137
|
|
|
124
138
|
### 모듈
|
|
125
139
|
- `ThrottlerModule.forRoot(options)`: 검증된 throttler 옵션과 `ThrottlerGuard`를 모듈 그래프에 제공합니다.
|
|
126
140
|
- 패키지 수준 등록은 `ThrottlerModule.forRoot(options)`를 통해 지원합니다. 내부 프로바이더 조합 헬퍼와 DI 토큰은 공개 계약에 포함되지 않습니다.
|
|
127
141
|
|
|
128
|
-
`ttl`과 `limit`은 양의 finite integer여야 합니다. `trustProxyHeaders`와 `keyGenerator`로 client identity를 조정할 수
|
|
142
|
+
`ttl`과 `limit`은 양의 finite integer여야 합니다. `global`은 기본값이 `true`입니다. throttler provider를 가져온 모듈 범위에만 유지하려면 `global: false`를 설정하세요. `trustProxyHeaders`와 `keyGenerator`로 client identity를 조정할 수 있으며, `keyGenerator`를 제공할 때는 함수여야 합니다. 모듈 옵션은 guard가 연결될 때 검증되고 값으로 캡처되므로, 호출자가 나중에 options 객체를 변경해도 실행 중인 throttling 정책은 바뀌지 않습니다. `store` 옵션을 제공하지 않으면 각 `ThrottlerGuard` 인스턴스가 자체 in-memory store를 소유합니다. 저장소를 공유하거나 외부에서 관리해야 한다면 `RedisThrottlerStore` 같은 `ThrottlerStore` 구현을 전달하세요.
|
|
129
143
|
|
|
130
144
|
### 데코레이터
|
|
131
145
|
- `@Throttle({ ttl, limit })`: 클래스나 메서드에 특정 속도 제한을 설정합니다.
|
|
132
146
|
- `@SkipThrottle()`: 클래스나 메서드에 대해 속도 제한을 비활성화합니다.
|
|
147
|
+
- `ThrottlerHandlerOptions`: `@Throttle(...)`이 받는 공개 `{ ttl, limit }` 정책 shape입니다. 두 값은 모두 양의 finite integer여야 하며, 메서드 수준 정책은 클래스 수준 정책보다 우선하고 클래스 수준 정책은 모듈 기본값보다 우선합니다.
|
|
133
148
|
- 기존 root-barrel metadata helper(`throttleRouteMetadataKey`, `getThrottleMetadata`, `getSkipThrottleMetadata`, `getClassThrottleMetadata`, `getClassSkipThrottleMetadata`)는 decorator metadata를 직접 검사하던 advanced integration과의 호환성을 위해 계속 export됩니다.
|
|
134
149
|
|
|
135
150
|
### 가드
|
|
@@ -138,8 +153,10 @@ ThrottlerModule.forRoot({
|
|
|
138
153
|
### 저장소(Store)
|
|
139
154
|
- `createMemoryThrottlerStore()`: 간단한 메모리 내 저장소를 생성합니다 (기본값).
|
|
140
155
|
- `RedisThrottlerStore`: Redis용 저장소 어댑터입니다.
|
|
156
|
+
- `RedisThrottlerClient`: `RedisThrottlerStore`가 받는 구조적 Redis command client 계약입니다.
|
|
141
157
|
- `ThrottlerStore`: custom store를 위한 공개 계약입니다.
|
|
142
158
|
- `ThrottlerConsumeInput`: custom store가 guard의 현재 시간과 TTL window를 공유할 수 있도록 `ThrottlerStore.consume(key, input)`에 전달되는 공개 입력 shape입니다.
|
|
159
|
+
- `ThrottlerStoreEntry`: `ThrottlerStore.consume(...)`이 반환하는 공개 결과 shape입니다. `count`는 현재 consume 이후 활성 window의 요청 수이고, `resetAt`은 `Retry-After` 계산에 사용하는 epoch millisecond reset 경계입니다. Custom store는 backing store clock이 애플리케이션 프로세스보다 더 authoritative할 때 선택적 `retryAfterMs`를 반환할 수 있으며, guard는 limit 초과 시 이를 `Retry-After` 계산에 사용합니다.
|
|
143
160
|
|
|
144
161
|
### status와 diagnostics
|
|
145
162
|
- `createThrottlerPlatformStatusSnapshot(...)`: 플랫폼 status snapshot을 생성합니다.
|
|
@@ -155,7 +172,7 @@ ThrottlerModule.forRoot({
|
|
|
155
172
|
## 관련 패키지
|
|
156
173
|
|
|
157
174
|
- `@fluojs/http`: HTTP 컨텍스트 및 예외 처리를 위해 필요합니다.
|
|
158
|
-
- `@fluojs/redis`: `RedisThrottlerStore`
|
|
175
|
+
- `@fluojs/redis`: `RedisThrottlerStore`를 위한 공식 Redis client 통합입니다. `ioredis`와 호환되는 구조적 client도 지원합니다.
|
|
159
176
|
|
|
160
177
|
## 예제 소스
|
|
161
178
|
|
package/README.md
CHANGED
|
@@ -12,12 +12,15 @@ Decorator-based rate limiting for fluo applications with in-memory and Redis sto
|
|
|
12
12
|
- [Common Patterns](#common-patterns)
|
|
13
13
|
- [Redis Storage](#redis-storage)
|
|
14
14
|
- [Custom Key Generation](#custom-key-generation)
|
|
15
|
+
- [NestJS Migration Boundaries](#nestjs-migration-boundaries)
|
|
15
16
|
- [Public API Overview](#public-api-overview)
|
|
16
17
|
- [Related Packages](#related-packages)
|
|
17
18
|
- [Example Sources](#example-sources)
|
|
18
19
|
|
|
19
20
|
## Installation
|
|
20
21
|
|
|
22
|
+
`@fluojs/throttler` declares `engines.node >=20.0.0` in its published package manifest.
|
|
23
|
+
|
|
21
24
|
```bash
|
|
22
25
|
npm install @fluojs/throttler
|
|
23
26
|
```
|
|
@@ -71,6 +74,8 @@ class AuthController {
|
|
|
71
74
|
|
|
72
75
|
For multi-instance deployments, use `RedisThrottlerStore` to share the rate limit state across all instances. Redis-backed windows are anchored to Redis server time, so distributed app nodes with clock skew still enforce one shared reset boundary.
|
|
73
76
|
|
|
77
|
+
`RedisThrottlerStore` accepts the package-local structural `RedisThrottlerClient` contract: a Redis command client with an `eval(script, numberOfKeys, ...args)` method. `@fluojs/redis`, `ioredis`, and compatible custom clients can be passed without making the root `@fluojs/throttler` import depend on a concrete `ioredis` constructor type.
|
|
78
|
+
|
|
74
79
|
```typescript
|
|
75
80
|
import { ThrottlerModule, RedisThrottlerStore } from '@fluojs/throttler';
|
|
76
81
|
import { REDIS_CLIENT } from '@fluojs/redis';
|
|
@@ -86,7 +91,7 @@ ThrottlerModule.forRoot({
|
|
|
86
91
|
});
|
|
87
92
|
```
|
|
88
93
|
|
|
89
|
-
You can also pass any object that implements the `ThrottlerStore` contract through the `store` option.
|
|
94
|
+
You can also pass any object that implements the `ThrottlerStore` contract through the `store` option. `ThrottlerModule.forRoot(...)` validates that a custom store exposes a `consume(...)` function before request handling starts.
|
|
90
95
|
|
|
91
96
|
### Custom Key Generation
|
|
92
97
|
|
|
@@ -119,17 +124,27 @@ ThrottlerModule.forRoot({
|
|
|
119
124
|
});
|
|
120
125
|
```
|
|
121
126
|
|
|
127
|
+
## NestJS Migration Boundaries
|
|
128
|
+
|
|
129
|
+
When migrating from `@nestjs/throttler`, treat `@fluojs/throttler` as an explicit guard-stage package rather than a drop-in global limiter:
|
|
130
|
+
|
|
131
|
+
- `ThrottlerModule.forRoot(...)` registers validated options and providers, but it does not automatically enforce throttling on every route. Activate `ThrottlerGuard` with Fluo guard metadata such as `@UseGuards(ThrottlerGuard)` wherever enforcement is required.
|
|
132
|
+
- The public policy shape is one module default plus class- or method-level `@Throttle({ ttl, limit })` overrides. Named multi-window definitions such as burst plus sustained limits require explicit composition through HTTP middleware, a custom `ThrottlerStore`, or an application-owned guard wrapper.
|
|
133
|
+
- Forwarded client IP headers are ignored by default. Enable `trustProxyHeaders: true` only behind a trusted proxy that overwrites `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`.
|
|
134
|
+
- The guaranteed limit-exceeded response contract is HTTP `429` with `Retry-After`. Additional rate-limit headers or response bodies should be added at the application boundary, for example with an exception filter.
|
|
135
|
+
|
|
122
136
|
## Public API Overview
|
|
123
137
|
|
|
124
138
|
### Modules
|
|
125
139
|
- `ThrottlerModule.forRoot(options)`: Provides validated throttler options and `ThrottlerGuard` to the module graph.
|
|
126
140
|
- Package-level registration is supported through `ThrottlerModule.forRoot(options)`. Internal provider-composition helpers and DI tokens are not part of the public contract.
|
|
127
141
|
|
|
128
|
-
`ttl` and `limit` must be positive finite integers. `trustProxyHeaders` and `keyGenerator` customize client identity. Module options are validated and captured by value when the guard is wired so later mutation of the caller's options object does not change live throttling policy. If no `store` option is supplied, each `ThrottlerGuard` instance owns its own in-memory store; pass a `ThrottlerStore` implementation such as `RedisThrottlerStore` when storage must be shared or externally managed.
|
|
142
|
+
`ttl` and `limit` must be positive finite integers. `global` defaults to `true`; set `global: false` when the throttler providers should stay scoped to the importing module. `trustProxyHeaders` and `keyGenerator` customize client identity; `keyGenerator`, when provided, must be a function. Module options are validated and captured by value when the guard is wired so later mutation of the caller's options object does not change live throttling policy. If no `store` option is supplied, each `ThrottlerGuard` instance owns its own in-memory store; pass a `ThrottlerStore` implementation such as `RedisThrottlerStore` when storage must be shared or externally managed.
|
|
129
143
|
|
|
130
144
|
### Decorators
|
|
131
145
|
- `@Throttle({ ttl, limit })`: Sets a specific rate limit for a class or method.
|
|
132
146
|
- `@SkipThrottle()`: Disables throttling for a class or method.
|
|
147
|
+
- `ThrottlerHandlerOptions`: Public `{ ttl, limit }` policy shape accepted by `@Throttle(...)`. Both values must be positive finite integers; method-level policies override class-level policies, which override module defaults.
|
|
133
148
|
- Existing root-barrel metadata helpers (`throttleRouteMetadataKey`, `getThrottleMetadata`, `getSkipThrottleMetadata`, `getClassThrottleMetadata`, and `getClassSkipThrottleMetadata`) remain exported for compatibility with advanced integrations that already inspect decorator metadata directly.
|
|
134
149
|
|
|
135
150
|
### Guards
|
|
@@ -138,8 +153,10 @@ ThrottlerModule.forRoot({
|
|
|
138
153
|
### Stores
|
|
139
154
|
- `createMemoryThrottlerStore()`: Creates a simple in-memory store (default).
|
|
140
155
|
- `RedisThrottlerStore`: Store adapter for Redis.
|
|
156
|
+
- `RedisThrottlerClient`: Structural Redis command client contract accepted by `RedisThrottlerStore`.
|
|
141
157
|
- `ThrottlerStore`: Public contract for custom stores.
|
|
142
158
|
- `ThrottlerConsumeInput`: Public input shape passed to `ThrottlerStore.consume(key, input)` so custom stores can share the guard's current time and TTL window.
|
|
159
|
+
- `ThrottlerStoreEntry`: Public result shape returned by `ThrottlerStore.consume(...)`; `count` is the post-consume request count for the active window and `resetAt` is the epoch-millisecond reset boundary used for `Retry-After` calculation. Custom stores may return optional `retryAfterMs` when a backing store has a more authoritative clock than the application process; the guard uses it for `Retry-After` when the limit is exceeded.
|
|
143
160
|
|
|
144
161
|
### Status and diagnostics
|
|
145
162
|
- `createThrottlerPlatformStatusSnapshot(...)`: Creates a platform status snapshot.
|
|
@@ -155,7 +172,7 @@ Method-level `@Throttle(...)` overrides class-level settings, class-level settin
|
|
|
155
172
|
## Related Packages
|
|
156
173
|
|
|
157
174
|
- `@fluojs/http`: Required for HTTP context and Exception handling.
|
|
158
|
-
- `@fluojs/redis`:
|
|
175
|
+
- `@fluojs/redis`: Official Redis client integration for `RedisThrottlerStore`; `ioredis` and compatible structural clients are also supported.
|
|
159
176
|
|
|
160
177
|
## Example Sources
|
|
161
178
|
|
package/dist/guard.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ import type { ThrottlerModuleOptions } from './types.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare class ThrottlerGuard implements Guard {
|
|
7
7
|
private readonly options;
|
|
8
|
+
private readonly resolvedPolicies;
|
|
8
9
|
private readonly store;
|
|
9
10
|
constructor(options: ThrottlerModuleOptions);
|
|
11
|
+
private getResolvedPolicy;
|
|
10
12
|
/**
|
|
11
13
|
* Evaluate whether the current request is still within its allowed rate-limit window.
|
|
12
14
|
*
|
package/dist/guard.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAoD,MAAM,cAAc,CAAC;AAa/G,OAAO,KAAK,EAAE,sBAAsB,EAAuC,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAoD,MAAM,cAAc,CAAC;AAa/G,OAAO,KAAK,EAAE,sBAAsB,EAAuC,MAAM,YAAY,CAAC;AA4D9F;;GAEG;AACH,qBACa,cAAe,YAAW,KAAK;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IAEjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+D;IAEhG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;gBAE3B,OAAO,EAAE,sBAAsB;IAO3C,OAAO,CAAC,iBAAiB;IAuCzB;;;;;;OAMG;IACG,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;CAkC3D"}
|
package/dist/guard.js
CHANGED
|
@@ -29,8 +29,7 @@ function defaultKeyGenerator(ctx, trustProxyHeaders) {
|
|
|
29
29
|
trustProxyHeaders
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
-
function buildStoreKey(
|
|
33
|
-
const encodedHandlerKey = encodeURIComponent(handlerKey);
|
|
32
|
+
function buildStoreKey(encodedHandlerKey, clientKey) {
|
|
34
33
|
const encodedClientKey = encodeURIComponent(clientKey);
|
|
35
34
|
return `throttler:${encodedHandlerKey}:${encodedClientKey}`;
|
|
36
35
|
}
|
|
@@ -39,7 +38,7 @@ function buildHandlerKey(handler) {
|
|
|
39
38
|
return [`method:${handler.route.method}`, `path:${encodeURIComponent(handler.route.path)}`, `version:${encodeURIComponent(version)}`, `handler:${encodeURIComponent(handler.methodName)}`].join('|');
|
|
40
39
|
}
|
|
41
40
|
function resolveRetryAfterSeconds(entry, now) {
|
|
42
|
-
const retryAfterMs = entry[throttlerRetryAfterMsSymbol];
|
|
41
|
+
const retryAfterMs = entry.retryAfterMs ?? entry[throttlerRetryAfterMsSymbol];
|
|
43
42
|
if (typeof retryAfterMs === 'number' && Number.isFinite(retryAfterMs)) {
|
|
44
43
|
return Math.max(1, Math.ceil(retryAfterMs / 1000));
|
|
45
44
|
}
|
|
@@ -55,12 +54,43 @@ class ThrottlerGuard {
|
|
|
55
54
|
[_ThrottlerGuard, _initClass] = _applyDecs(this, [Inject(THROTTLER_OPTIONS)], []).c;
|
|
56
55
|
}
|
|
57
56
|
options;
|
|
57
|
+
resolvedPolicies = new WeakMap();
|
|
58
58
|
store;
|
|
59
59
|
constructor(options) {
|
|
60
60
|
const validatedOptions = validateThrottlerModuleOptions(options);
|
|
61
61
|
this.options = validatedOptions;
|
|
62
62
|
this.store = validatedOptions.store ?? createMemoryThrottlerStore();
|
|
63
63
|
}
|
|
64
|
+
getResolvedPolicy(handler) {
|
|
65
|
+
let controllerPolicies = this.resolvedPolicies.get(handler.controllerToken);
|
|
66
|
+
if (!controllerPolicies) {
|
|
67
|
+
controllerPolicies = new Map();
|
|
68
|
+
this.resolvedPolicies.set(handler.controllerToken, controllerPolicies);
|
|
69
|
+
}
|
|
70
|
+
const version = handler.route.version ?? handler.metadata.effectiveVersion ?? 'unversioned';
|
|
71
|
+
const cacheKey = [handler.methodName, handler.route.method, handler.route.path, version].join('\u0000');
|
|
72
|
+
const cachedPolicy = controllerPolicies.get(cacheKey);
|
|
73
|
+
if (cachedPolicy) {
|
|
74
|
+
return cachedPolicy;
|
|
75
|
+
}
|
|
76
|
+
const classBag = getClassMetadataBag(handler.controllerToken);
|
|
77
|
+
const methodBag = getMethodMetadataBag(handler.controllerToken, handler.methodName);
|
|
78
|
+
const skip = (classBag ? getClassSkipThrottleMetadata(classBag) : false) || (methodBag ? getSkipThrottleMetadata(methodBag) : false);
|
|
79
|
+
const methodThrottle = methodBag ? getThrottleMetadata(methodBag) : undefined;
|
|
80
|
+
const classThrottle = classBag ? getClassThrottleMetadata(classBag) : undefined;
|
|
81
|
+
const resolvedThrottle = validateThrottleOptions({
|
|
82
|
+
limit: methodThrottle?.limit ?? classThrottle?.limit ?? this.options.limit,
|
|
83
|
+
ttl: methodThrottle?.ttl ?? classThrottle?.ttl ?? this.options.ttl
|
|
84
|
+
});
|
|
85
|
+
const policy = {
|
|
86
|
+
encodedHandlerKey: encodeURIComponent(buildHandlerKey(handler)),
|
|
87
|
+
limit: resolvedThrottle.limit,
|
|
88
|
+
skip,
|
|
89
|
+
ttlSeconds: resolvedThrottle.ttl
|
|
90
|
+
};
|
|
91
|
+
controllerPolicies.set(cacheKey, policy);
|
|
92
|
+
return policy;
|
|
93
|
+
}
|
|
64
94
|
|
|
65
95
|
/**
|
|
66
96
|
* Evaluate whether the current request is still within its allowed rate-limit window.
|
|
@@ -74,37 +104,25 @@ class ThrottlerGuard {
|
|
|
74
104
|
handler,
|
|
75
105
|
requestContext
|
|
76
106
|
} = context;
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const classSkip = classBag ? getClassSkipThrottleMetadata(classBag) : false;
|
|
80
|
-
const methodSkip = methodBag ? getSkipThrottleMetadata(methodBag) : false;
|
|
81
|
-
if (classSkip || methodSkip) {
|
|
107
|
+
const policy = this.getResolvedPolicy(handler);
|
|
108
|
+
if (policy.skip) {
|
|
82
109
|
return true;
|
|
83
110
|
}
|
|
84
|
-
const methodThrottle = methodBag ? getThrottleMetadata(methodBag) : undefined;
|
|
85
|
-
const classThrottle = classBag ? getClassThrottleMetadata(classBag) : undefined;
|
|
86
|
-
const resolvedThrottle = validateThrottleOptions({
|
|
87
|
-
limit: methodThrottle?.limit ?? classThrottle?.limit ?? this.options.limit,
|
|
88
|
-
ttl: methodThrottle?.ttl ?? classThrottle?.ttl ?? this.options.ttl
|
|
89
|
-
});
|
|
90
|
-
const ttlSeconds = resolvedThrottle.ttl;
|
|
91
|
-
const limit = resolvedThrottle.limit;
|
|
92
111
|
const middlewareCtx = {
|
|
93
112
|
request: requestContext.request,
|
|
94
113
|
requestContext,
|
|
95
114
|
response: requestContext.response
|
|
96
115
|
};
|
|
97
116
|
const clientKey = this.options.keyGenerator ? this.options.keyGenerator(middlewareCtx) : defaultKeyGenerator(middlewareCtx, this.options.trustProxyHeaders ?? false);
|
|
98
|
-
const
|
|
99
|
-
const storeKey = buildStoreKey(handlerKey, clientKey);
|
|
117
|
+
const storeKey = buildStoreKey(policy.encodedHandlerKey, clientKey);
|
|
100
118
|
const now = Date.now();
|
|
101
119
|
const rawEntry = await this.store.consume(storeKey, {
|
|
102
120
|
now,
|
|
103
|
-
ttlSeconds
|
|
121
|
+
ttlSeconds: policy.ttlSeconds
|
|
104
122
|
});
|
|
105
123
|
const entry = validateThrottlerStoreEntry(rawEntry);
|
|
106
|
-
if (entry.count > limit) {
|
|
107
|
-
const retryAfter = resolveRetryAfterSeconds(
|
|
124
|
+
if (entry.count > policy.limit) {
|
|
125
|
+
const retryAfter = resolveRetryAfterSeconds(entry, now);
|
|
108
126
|
requestContext.response.setHeader('Retry-After', String(retryAfter));
|
|
109
127
|
throw new TooManyRequestsException('Too Many Requests', {
|
|
110
128
|
meta: {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { getClassSkipThrottleMetadata, getClassThrottleMetadata, getSkipThrottle
|
|
|
2
2
|
export { ThrottlerGuard } from './guard.js';
|
|
3
3
|
export * from './module.js';
|
|
4
4
|
export { RedisThrottlerStore } from './redis-store.js';
|
|
5
|
+
export type { RedisThrottlerClient } from './redis-store.js';
|
|
5
6
|
export * from './status.js';
|
|
6
7
|
export { createMemoryThrottlerStore } from './store.js';
|
|
7
8
|
export type { ThrottlerConsumeInput, ThrottlerHandlerOptions, ThrottlerModuleOptions, ThrottlerStore, ThrottlerStoreEntry, } from './types.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,EACnB,YAAY,EACZ,QAAQ,EACR,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AACxD,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,EACnB,YAAY,EACZ,QAAQ,EACR,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AACxD,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,sBAAsB,EACtB,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|
package/dist/module.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { type ModuleType } from '@fluojs/runtime';
|
|
2
2
|
import type { ThrottlerModuleOptions } from './types.js';
|
|
3
3
|
/**
|
|
4
|
-
* Runtime module entrypoint for
|
|
4
|
+
* Runtime module entrypoint for throttler policy and guard provider registration.
|
|
5
5
|
*
|
|
6
6
|
* @remarks
|
|
7
|
-
* The module
|
|
8
|
-
*
|
|
7
|
+
* The module registers validated throttler options and makes `ThrottlerGuard`
|
|
8
|
+
* injectable. Routes still opt in to enforcement explicitly with guard metadata
|
|
9
|
+
* such as `@UseGuards(ThrottlerGuard)`.
|
|
9
10
|
*/
|
|
10
11
|
export declare class ThrottlerModule {
|
|
11
12
|
/**
|
|
12
|
-
* Register
|
|
13
|
+
* Register throttler providers with validated module options.
|
|
13
14
|
*
|
|
14
15
|
* @param options Module-wide throttling policy.
|
|
15
|
-
* @returns A runtime module exporting `ThrottlerGuard
|
|
16
|
+
* @returns A runtime module exporting `ThrottlerGuard` for explicit route activation.
|
|
16
17
|
*
|
|
17
18
|
* @example
|
|
18
19
|
* ```ts
|
package/dist/module.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAkBzD
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAkBzD;;;;;;;GAOG;AACH,qBAAa,eAAe;IAC1B;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,sBAAsB,GAAG,UAAU;CAS5D"}
|
package/dist/module.js
CHANGED
|
@@ -14,18 +14,19 @@ function createThrottlerProviders(options) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Runtime module entrypoint for
|
|
17
|
+
* Runtime module entrypoint for throttler policy and guard provider registration.
|
|
18
18
|
*
|
|
19
19
|
* @remarks
|
|
20
|
-
* The module
|
|
21
|
-
*
|
|
20
|
+
* The module registers validated throttler options and makes `ThrottlerGuard`
|
|
21
|
+
* injectable. Routes still opt in to enforcement explicitly with guard metadata
|
|
22
|
+
* such as `@UseGuards(ThrottlerGuard)`.
|
|
22
23
|
*/
|
|
23
24
|
export class ThrottlerModule {
|
|
24
25
|
/**
|
|
25
|
-
* Register
|
|
26
|
+
* Register throttler providers with validated module options.
|
|
26
27
|
*
|
|
27
28
|
* @param options Module-wide throttling policy.
|
|
28
|
-
* @returns A runtime module exporting `ThrottlerGuard
|
|
29
|
+
* @returns A runtime module exporting `ThrottlerGuard` for explicit route activation.
|
|
29
30
|
*
|
|
30
31
|
* @example
|
|
31
32
|
* ```ts
|
package/dist/redis-store.d.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
import type Redis from 'ioredis';
|
|
2
1
|
import type { ThrottlerConsumeInput, ThrottlerStore, ThrottlerStoreEntry } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Structural Redis command client accepted by `RedisThrottlerStore`.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* This package-local contract keeps the throttler root API independent of
|
|
7
|
+
* concrete Redis client packages while remaining compatible with `ioredis`,
|
|
8
|
+
* `@fluojs/redis`, and custom clients that expose Redis `EVAL`.
|
|
9
|
+
*/
|
|
10
|
+
export interface RedisThrottlerClient {
|
|
11
|
+
/**
|
|
12
|
+
* Evaluate a Redis Lua script.
|
|
13
|
+
*
|
|
14
|
+
* @param script Lua script to evaluate.
|
|
15
|
+
* @param numberOfKeys Number of key arguments Redis should treat as `KEYS`.
|
|
16
|
+
* @param args Key and argument values passed to Redis `EVAL`.
|
|
17
|
+
* @returns The raw Redis response for the script.
|
|
18
|
+
*/
|
|
19
|
+
eval(script: string, numberOfKeys: number, ...args: string[]): unknown | Promise<unknown>;
|
|
20
|
+
}
|
|
3
21
|
/**
|
|
4
22
|
* Redis-backed throttler store for distributed rate limits.
|
|
5
23
|
*
|
|
@@ -9,7 +27,12 @@ import type { ThrottlerConsumeInput, ThrottlerStore, ThrottlerStoreEntry } from
|
|
|
9
27
|
*/
|
|
10
28
|
export declare class RedisThrottlerStore implements ThrottlerStore {
|
|
11
29
|
private readonly client;
|
|
12
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Create a Redis-backed throttler store.
|
|
32
|
+
*
|
|
33
|
+
* @param client Redis command client that supports `EVAL`.
|
|
34
|
+
*/
|
|
35
|
+
constructor(client: RedisThrottlerClient);
|
|
13
36
|
/**
|
|
14
37
|
* Consume one throttle slot for the provided key.
|
|
15
38
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-store.d.ts","sourceRoot":"","sources":["../src/redis-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"redis-store.d.ts","sourceRoot":"","sources":["../src/redis-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAG7F;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;;OAOG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3F;AAmED;;;;;;GAMG;AACH,qBAAa,mBAAoB,YAAW,cAAc;IAM5C,OAAO,CAAC,QAAQ,CAAC,MAAM;IALnC;;;;OAIG;gBAC0B,MAAM,EAAE,oBAAoB;IAEzD;;;;;;OAMG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAUvF"}
|
package/dist/redis-store.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { throttlerRetryAfterMsSymbol } from './store-internals.js';
|
|
2
2
|
import { validateThrottlerStoreEntry } from './validation.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structural Redis command client accepted by `RedisThrottlerStore`.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* This package-local contract keeps the throttler root API independent of
|
|
9
|
+
* concrete Redis client packages while remaining compatible with `ioredis`,
|
|
10
|
+
* `@fluojs/redis`, and custom clients that expose Redis `EVAL`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
3
13
|
const CONSUME_LUA = ["local key = KEYS[1]", "local ttlMs = tonumber(ARGV[1])", "local time = redis.call('TIME')", "local nowSeconds = tonumber(time[1])", "local nowMicros = tonumber(time[2])", 'local now = math.floor((nowSeconds * 1000) + (nowMicros / 1000))', "local raw = redis.call('GET', key)", "local count", "local resetAt", 'if not raw then', ' count = 1', ' resetAt = now + ttlMs', 'else', ' local decoded = cjson.decode(raw)', " count = tonumber(decoded['count']) or 0", " resetAt = tonumber(decoded['resetAt']) or (now + ttlMs)", ' if now >= resetAt then', ' count = 1', ' resetAt = now + ttlMs', ' else', ' count = count + 1', ' end', 'end', 'local ttlMsLeft = math.max(resetAt - now, 1)', "redis.call('SET', key, cjson.encode({ count = count, resetAt = resetAt }), 'PX', ttlMsLeft)", 'return {count, resetAt, ttlMsLeft}'].join('\n');
|
|
4
14
|
function parseConsumeResult(result) {
|
|
5
15
|
if (!Array.isArray(result) || result.length < 2) {
|
|
@@ -11,7 +21,11 @@ function parseConsumeResult(result) {
|
|
|
11
21
|
if (!Number.isFinite(count) || !Number.isFinite(resetAt)) {
|
|
12
22
|
throw new Error('Redis throttler consume script returned non-numeric counters.');
|
|
13
23
|
}
|
|
14
|
-
const entry = validateThrottlerStoreEntry({
|
|
24
|
+
const entry = Number.isFinite(retryAfterMs) ? validateThrottlerStoreEntry({
|
|
25
|
+
count,
|
|
26
|
+
resetAt,
|
|
27
|
+
retryAfterMs
|
|
28
|
+
}) : validateThrottlerStoreEntry({
|
|
15
29
|
count,
|
|
16
30
|
resetAt
|
|
17
31
|
});
|
|
@@ -34,6 +48,11 @@ function parseConsumeResult(result) {
|
|
|
34
48
|
* requests across instances observe the same counter and reset window.
|
|
35
49
|
*/
|
|
36
50
|
export class RedisThrottlerStore {
|
|
51
|
+
/**
|
|
52
|
+
* Create a Redis-backed throttler store.
|
|
53
|
+
*
|
|
54
|
+
* @param client Redis command client that supports `EVAL`.
|
|
55
|
+
*/
|
|
37
56
|
constructor(client) {
|
|
38
57
|
this.client = client;
|
|
39
58
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { MiddlewareContext } from '@fluojs/http';
|
|
2
2
|
/**
|
|
3
|
-
* Snapshot of a client's current rate-limit window state.
|
|
3
|
+
* Snapshot of a client's current rate-limit window state returned by a throttler store.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Stores return this value after a `consume(...)` operation. `ThrottlerGuard`
|
|
7
|
+
* compares `count` with the resolved request limit and uses `resetAt` to compute
|
|
8
|
+
* the `Retry-After` value when the limit is exceeded. Custom stores that have
|
|
9
|
+
* a more authoritative server-side clock may return `retryAfterMs` to override
|
|
10
|
+
* that calculation without relying on a private package symbol.
|
|
4
11
|
*/
|
|
5
12
|
export interface ThrottlerStoreEntry {
|
|
13
|
+
/** Number of requests recorded in the active window after the current consume operation. */
|
|
6
14
|
count: number;
|
|
15
|
+
/** Epoch time in milliseconds when the active rate-limit window resets. */
|
|
7
16
|
resetAt: number;
|
|
17
|
+
/** Optional authoritative milliseconds until retry, usually from a backing store server clock. */
|
|
18
|
+
retryAfterMs?: number;
|
|
8
19
|
}
|
|
9
20
|
/**
|
|
10
21
|
* Public input passed to a `ThrottlerStore` when consuming a request slot.
|
|
@@ -26,7 +37,11 @@ export interface ThrottlerStore {
|
|
|
26
37
|
consume(key: string, input: ThrottlerConsumeInput): ThrottlerStoreEntry | Promise<ThrottlerStoreEntry>;
|
|
27
38
|
}
|
|
28
39
|
/**
|
|
29
|
-
* Per-handler or per-controller throttle
|
|
40
|
+
* Per-handler or per-controller throttle policy accepted by `@Throttle(...)`.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Method-level policies override class-level policies, and class-level policies
|
|
44
|
+
* override module defaults. Both values must be positive finite integers.
|
|
30
45
|
*/
|
|
31
46
|
export interface ThrottlerHandlerOptions {
|
|
32
47
|
/** Seconds in the rate-limit window. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACxG;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,MAAM,CAAC;IAClD,+DAA+D;IAC/D,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB"}
|
package/dist/validation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA0BvG;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAOjG;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,sBAAsB,GAAG,sBAAsB,CAqBtG;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAkB3F"}
|
package/dist/validation.js
CHANGED
|
@@ -8,6 +8,11 @@ function assertFiniteInteger(value, field) {
|
|
|
8
8
|
throw new Error(`Invalid throttler ${field}: expected a finite integer.`);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
function assertNonNegativeFiniteInteger(value, field) {
|
|
12
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
13
|
+
throw new Error(`Invalid throttler ${field}: expected a non-negative finite integer.`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
11
16
|
function assertOptionalBoolean(value, field) {
|
|
12
17
|
if (value !== undefined && typeof value !== 'boolean') {
|
|
13
18
|
throw new Error(`Invalid throttler ${field}: expected a boolean when provided.`);
|
|
@@ -39,6 +44,12 @@ export function validateThrottlerModuleOptions(options) {
|
|
|
39
44
|
validateThrottleOptions(options);
|
|
40
45
|
assertOptionalBoolean(options.global, 'global');
|
|
41
46
|
assertOptionalBoolean(options.trustProxyHeaders, 'trustProxyHeaders');
|
|
47
|
+
if (options.keyGenerator !== undefined && typeof options.keyGenerator !== 'function') {
|
|
48
|
+
throw new Error('Invalid throttler keyGenerator: expected a function when provided.');
|
|
49
|
+
}
|
|
50
|
+
if (options.store !== undefined && (options.store === null || typeof options.store.consume !== 'function')) {
|
|
51
|
+
throw new Error('Invalid throttler store.consume: expected a function when store is provided.');
|
|
52
|
+
}
|
|
42
53
|
return {
|
|
43
54
|
global: options.global,
|
|
44
55
|
keyGenerator: options.keyGenerator,
|
|
@@ -58,8 +69,15 @@ export function validateThrottlerModuleOptions(options) {
|
|
|
58
69
|
export function validateThrottlerStoreEntry(entry) {
|
|
59
70
|
assertPositiveFiniteInteger(entry.count, 'store count');
|
|
60
71
|
assertFiniteInteger(entry.resetAt, 'store resetAt');
|
|
61
|
-
|
|
72
|
+
if (entry.retryAfterMs !== undefined) {
|
|
73
|
+
assertNonNegativeFiniteInteger(entry.retryAfterMs, 'store retryAfterMs');
|
|
74
|
+
}
|
|
75
|
+
const validatedEntry = {
|
|
62
76
|
count: entry.count,
|
|
63
77
|
resetAt: entry.resetAt
|
|
64
78
|
};
|
|
79
|
+
if (entry.retryAfterMs !== undefined) {
|
|
80
|
+
validatedEntry.retryAfterMs = entry.retryAfterMs;
|
|
81
|
+
}
|
|
82
|
+
return validatedEntry;
|
|
65
83
|
}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"redis",
|
|
10
10
|
"decorator"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.4",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@fluojs/core": "^1.0.
|
|
40
|
-
"@fluojs/di": "^1.0
|
|
41
|
-
"@fluojs/http": "^1.
|
|
42
|
-
"@fluojs/runtime": "^1.1.
|
|
39
|
+
"@fluojs/core": "^1.0.3",
|
|
40
|
+
"@fluojs/di": "^1.1.0",
|
|
41
|
+
"@fluojs/http": "^1.1.2",
|
|
42
|
+
"@fluojs/runtime": "^1.1.8"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"ioredis": "^5.0.0",
|
|
46
|
-
"@fluojs/redis": "^1.0.
|
|
46
|
+
"@fluojs/redis": "^1.0.2"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
49
49
|
"@fluojs/redis": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"ioredis": "^5.10.0",
|
|
58
58
|
"vitest": "^3.2.4",
|
|
59
|
-
"@fluojs/testing": "^1.0.
|
|
59
|
+
"@fluojs/testing": "^1.0.6"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|