@fluojs/throttler 1.0.2 → 1.0.3
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 +1 -1
- package/README.md +1 -1
- package/dist/guard.d.ts +2 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +37 -19
- package/package.json +7 -7
package/README.ko.md
CHANGED
|
@@ -125,7 +125,7 @@ ThrottlerModule.forRoot({
|
|
|
125
125
|
- `ThrottlerModule.forRoot(options)`: 검증된 throttler 옵션과 `ThrottlerGuard`를 모듈 그래프에 제공합니다.
|
|
126
126
|
- 패키지 수준 등록은 `ThrottlerModule.forRoot(options)`를 통해 지원합니다. 내부 프로바이더 조합 헬퍼와 DI 토큰은 공개 계약에 포함되지 않습니다.
|
|
127
127
|
|
|
128
|
-
`ttl`과 `limit`은 양의 finite integer여야 합니다. `trustProxyHeaders`와 `keyGenerator`로 client identity를 조정할 수 있습니다. 모듈 옵션은 guard가 연결될 때 검증되고 값으로 캡처되므로, 호출자가 나중에 options 객체를 변경해도 실행 중인 throttling 정책은 바뀌지 않습니다. `store` 옵션을 제공하지 않으면 각 `ThrottlerGuard` 인스턴스가 자체 in-memory store를 소유합니다. 저장소를 공유하거나 외부에서 관리해야 한다면 `RedisThrottlerStore` 같은 `ThrottlerStore` 구현을 전달하세요.
|
|
128
|
+
`ttl`과 `limit`은 양의 finite integer여야 합니다. `global`은 기본값이 `true`입니다. throttler provider를 가져온 모듈 범위에만 유지하려면 `global: false`를 설정하세요. `trustProxyHeaders`와 `keyGenerator`로 client identity를 조정할 수 있습니다. 모듈 옵션은 guard가 연결될 때 검증되고 값으로 캡처되므로, 호출자가 나중에 options 객체를 변경해도 실행 중인 throttling 정책은 바뀌지 않습니다. `store` 옵션을 제공하지 않으면 각 `ThrottlerGuard` 인스턴스가 자체 in-memory store를 소유합니다. 저장소를 공유하거나 외부에서 관리해야 한다면 `RedisThrottlerStore` 같은 `ThrottlerStore` 구현을 전달하세요.
|
|
129
129
|
|
|
130
130
|
### 데코레이터
|
|
131
131
|
- `@Throttle({ ttl, limit })`: 클래스나 메서드에 특정 속도 제한을 설정합니다.
|
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ ThrottlerModule.forRoot({
|
|
|
125
125
|
- `ThrottlerModule.forRoot(options)`: Provides validated throttler options and `ThrottlerGuard` to the module graph.
|
|
126
126
|
- Package-level registration is supported through `ThrottlerModule.forRoot(options)`. Internal provider-composition helpers and DI tokens are not part of the public contract.
|
|
127
127
|
|
|
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.
|
|
128
|
+
`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. 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
129
|
|
|
130
130
|
### Decorators
|
|
131
131
|
- `@Throttle({ ttl, limit })`: Sets a specific rate limit for a class or method.
|
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;AA6D9F;;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
|
}
|
|
@@ -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,36 +104,24 @@ 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) {
|
|
124
|
+
if (entry.count > policy.limit) {
|
|
107
125
|
const retryAfter = resolveRetryAfterSeconds(rawEntry, now);
|
|
108
126
|
requestContext.response.setHeader('Retry-After', String(retryAfter));
|
|
109
127
|
throw new TooManyRequestsException('Too Many Requests', {
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"redis",
|
|
10
10
|
"decorator"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.3",
|
|
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/
|
|
41
|
-
"@fluojs/
|
|
42
|
-
"@fluojs/runtime": "^1.1.
|
|
39
|
+
"@fluojs/core": "^1.0.3",
|
|
40
|
+
"@fluojs/http": "^1.1.0",
|
|
41
|
+
"@fluojs/di": "^1.1.0",
|
|
42
|
+
"@fluojs/runtime": "^1.1.6"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"ioredis": "^5.0.0",
|
|
46
|
-
"@fluojs/redis": "^1.0.
|
|
46
|
+
"@fluojs/redis": "^1.0.1"
|
|
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.5"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|