@fluojs/throttler 1.0.1 → 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 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 })`: 클래스나 메서드에 특정 속도 제한을 설정합니다.
@@ -144,6 +144,11 @@ ThrottlerModule.forRoot({
144
144
  ### status와 diagnostics
145
145
  - `createThrottlerPlatformStatusSnapshot(...)`: 플랫폼 status snapshot을 생성합니다.
146
146
  - `createThrottlerPlatformDiagnosticIssues(...)`: 잘못된 throttler 상태에 대한 diagnostic issue를 생성합니다.
147
+ - `ThrottlerStatusAdapterInput`: status 및 diagnostic helper의 공개 입력 shape입니다. 부트스트랩 중 수집한 store kind, ownership mode, operation mode, backing-store readiness, dependency linkage, readiness criticality 힌트를 전달합니다.
148
+ - `ThrottlerPlatformStatusSnapshot`: `createThrottlerPlatformStatusSnapshot(...)`이 반환하는 공개 출력 shape입니다. 런타임 platform snapshot과 호환되는 readiness, health, ownership, details 섹션을 포함합니다.
149
+ - `ThrottlerStoreKind`: status adapter가 인식하는 store category입니다: `memory`, `redis`, `custom`.
150
+ - `ThrottlerStoreOwnershipMode`: status snapshot에 보고되는 ownership mode입니다: guard가 소유한 리소스는 `framework`, 외부에서 관리되는 store는 `external`입니다.
151
+ - `ThrottlerOperationMode`: status details에 보고되는 operation mode입니다: `local-only`, `distributed`, `local-fallback`, `custom`.
147
152
 
148
153
  메서드 수준 `@Throttle(...)`은 클래스 수준 설정보다 우선하고, 클래스 수준 설정은 모듈 기본값보다 우선합니다. `@SkipThrottle()`은 클래스나 메서드 수준 모두에서 throttling을 우회합니다.
149
154
 
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.
@@ -144,6 +144,11 @@ ThrottlerModule.forRoot({
144
144
  ### Status and diagnostics
145
145
  - `createThrottlerPlatformStatusSnapshot(...)`: Creates a platform status snapshot.
146
146
  - `createThrottlerPlatformDiagnosticIssues(...)`: Creates diagnostic issues for invalid throttler state.
147
+ - `ThrottlerStatusAdapterInput`: Public input shape for status and diagnostic helpers. It carries store kind, ownership mode, operation mode, backing-store readiness, dependency linkage, and readiness criticality hints collected during bootstrap.
148
+ - `ThrottlerPlatformStatusSnapshot`: Public output shape returned by `createThrottlerPlatformStatusSnapshot(...)`, containing readiness, health, ownership, and details sections compatible with runtime platform snapshots.
149
+ - `ThrottlerStoreKind`: Store categories recognized by the status adapter: `memory`, `redis`, or `custom`.
150
+ - `ThrottlerStoreOwnershipMode`: Ownership modes reported in status snapshots: `framework` for guard-owned resources or `external` for externally managed stores.
151
+ - `ThrottlerOperationMode`: Operation modes reported in status details: `local-only`, `distributed`, `local-fallback`, or `custom`.
147
152
 
148
153
  Method-level `@Throttle(...)` overrides class-level settings, class-level settings override module defaults, and `@SkipThrottle()` bypasses throttling at either class or method level.
149
154
 
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
  *
@@ -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;AAuD9F;;GAEG;AACH,qBACa,cAAe,YAAW,KAAK;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IAEjD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;gBAE3B,OAAO,EAAE,sBAAsB;IAO3C;;;;;;OAMG;IACG,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;CAkD3D"}
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(handlerKey, clientKey) {
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 classBag = getClassMetadataBag(handler.controllerToken);
78
- const methodBag = getMethodMetadataBag(handler.controllerToken, handler.methodName);
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 handlerKey = buildHandlerKey(handler);
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.1",
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.1",
40
- "@fluojs/di": "^1.0.1",
41
- "@fluojs/http": "^1.0.0",
42
- "@fluojs/runtime": "^1.0.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.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.1"
59
+ "@fluojs/testing": "^1.0.5"
60
60
  },
61
61
  "scripts": {
62
62
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",