@velajs/vela 0.3.0 → 0.4.1
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/health/health.http.d.ts +14 -0
- package/dist/health/health.http.js +75 -0
- package/dist/health/health.indicator.d.ts +9 -0
- package/dist/health/health.indicator.js +32 -0
- package/dist/health/health.module.d.ts +2 -0
- package/dist/health/health.module.js +26 -0
- package/dist/health/health.service.d.ts +7 -0
- package/dist/health/health.service.js +65 -0
- package/dist/health/health.types.d.ts +15 -0
- package/dist/health/health.types.js +1 -0
- package/dist/health/index.d.ts +6 -0
- package/dist/health/index.js +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/throttler/index.d.ts +6 -0
- package/dist/throttler/index.js +5 -0
- package/dist/throttler/throttler.decorators.d.ts +3 -0
- package/dist/throttler/throttler.decorators.js +4 -0
- package/dist/throttler/throttler.guard.d.ts +9 -0
- package/dist/throttler/throttler.guard.js +72 -0
- package/dist/throttler/throttler.module.d.ts +5 -0
- package/dist/throttler/throttler.module.js +43 -0
- package/dist/throttler/throttler.storage.d.ts +10 -0
- package/dist/throttler/throttler.storage.js +44 -0
- package/dist/throttler/throttler.tokens.d.ts +6 -0
- package/dist/throttler/throttler.tokens.js +5 -0
- package/dist/throttler/throttler.types.d.ts +25 -0
- package/dist/throttler/throttler.types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { HealthIndicatorService } from './health.indicator';
|
|
2
|
+
import type { HealthIndicatorResult, ResponseCheckCallback } from './health.types';
|
|
3
|
+
export interface HttpPingOptions {
|
|
4
|
+
method?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
expectedStatus?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class HttpHealthIndicator {
|
|
10
|
+
private indicator;
|
|
11
|
+
constructor(indicator: HealthIndicatorService);
|
|
12
|
+
pingCheck(key: string, url: string, options?: HttpPingOptions): Promise<HealthIndicatorResult>;
|
|
13
|
+
responseCheck(key: string, url: string, callback: ResponseCheckCallback, options?: HttpPingOptions): Promise<HealthIndicatorResult>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
import { Injectable } from "../container/index.js";
|
|
11
|
+
import { HealthIndicatorService } from "./health.indicator.js";
|
|
12
|
+
export class HttpHealthIndicator {
|
|
13
|
+
indicator;
|
|
14
|
+
constructor(indicator){
|
|
15
|
+
this.indicator = indicator;
|
|
16
|
+
}
|
|
17
|
+
async pingCheck(key, url, options) {
|
|
18
|
+
const { method = 'GET', headers, timeout = 5000, expectedStatus } = options ?? {};
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(url, {
|
|
21
|
+
method,
|
|
22
|
+
headers,
|
|
23
|
+
signal: AbortSignal.timeout(timeout)
|
|
24
|
+
});
|
|
25
|
+
const statusCode = response.status;
|
|
26
|
+
const isHealthy = expectedStatus !== undefined ? statusCode === expectedStatus : statusCode >= 200 && statusCode < 300;
|
|
27
|
+
if (isHealthy) {
|
|
28
|
+
return this.indicator.check(key).up({
|
|
29
|
+
statusCode
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return this.indicator.check(key).down({
|
|
33
|
+
statusCode,
|
|
34
|
+
message: response.statusText
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
38
|
+
return this.indicator.check(key).down({
|
|
39
|
+
message
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async responseCheck(key, url, callback, options) {
|
|
44
|
+
const { method = 'GET', headers, timeout = 5000 } = options ?? {};
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
method,
|
|
48
|
+
headers,
|
|
49
|
+
signal: AbortSignal.timeout(timeout)
|
|
50
|
+
});
|
|
51
|
+
const statusCode = response.status;
|
|
52
|
+
const isHealthy = await callback(response);
|
|
53
|
+
if (isHealthy) {
|
|
54
|
+
return this.indicator.check(key).up({
|
|
55
|
+
statusCode
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return this.indicator.check(key).down({
|
|
59
|
+
statusCode
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
63
|
+
return this.indicator.check(key).down({
|
|
64
|
+
message
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
HttpHealthIndicator = _ts_decorate([
|
|
70
|
+
Injectable(),
|
|
71
|
+
_ts_metadata("design:type", Function),
|
|
72
|
+
_ts_metadata("design:paramtypes", [
|
|
73
|
+
typeof HealthIndicatorService === "undefined" ? Object : HealthIndicatorService
|
|
74
|
+
])
|
|
75
|
+
], HttpHealthIndicator);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HealthIndicatorResult } from './health.types';
|
|
2
|
+
interface HealthIndicatorBuilder {
|
|
3
|
+
up(data?: Record<string, unknown>): HealthIndicatorResult;
|
|
4
|
+
down(data?: Record<string, unknown>): HealthIndicatorResult;
|
|
5
|
+
}
|
|
6
|
+
export declare class HealthIndicatorService {
|
|
7
|
+
check(key: string): HealthIndicatorBuilder;
|
|
8
|
+
}
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
import { Injectable } from "../container/index.js";
|
|
8
|
+
export class HealthIndicatorService {
|
|
9
|
+
check(key) {
|
|
10
|
+
return {
|
|
11
|
+
up (data) {
|
|
12
|
+
return {
|
|
13
|
+
[key]: {
|
|
14
|
+
status: 'up',
|
|
15
|
+
...data
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
down (data) {
|
|
20
|
+
return {
|
|
21
|
+
[key]: {
|
|
22
|
+
status: 'down',
|
|
23
|
+
...data
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
HealthIndicatorService = _ts_decorate([
|
|
31
|
+
Injectable()
|
|
32
|
+
], HealthIndicatorService);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
import { Module } from "../module/index.js";
|
|
8
|
+
import { HealthCheckService } from "./health.service.js";
|
|
9
|
+
import { HealthIndicatorService } from "./health.indicator.js";
|
|
10
|
+
import { HttpHealthIndicator } from "./health.http.js";
|
|
11
|
+
export class HealthModule {
|
|
12
|
+
}
|
|
13
|
+
HealthModule = _ts_decorate([
|
|
14
|
+
Module({
|
|
15
|
+
providers: [
|
|
16
|
+
HealthCheckService,
|
|
17
|
+
HealthIndicatorService,
|
|
18
|
+
HttpHealthIndicator
|
|
19
|
+
],
|
|
20
|
+
exports: [
|
|
21
|
+
HealthCheckService,
|
|
22
|
+
HealthIndicatorService,
|
|
23
|
+
HttpHealthIndicator
|
|
24
|
+
]
|
|
25
|
+
})
|
|
26
|
+
], HealthModule);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BeforeApplicationShutdown } from '../lifecycle/index';
|
|
2
|
+
import type { HealthCheckResult, HealthIndicatorFunction } from './health.types';
|
|
3
|
+
export declare class HealthCheckService implements BeforeApplicationShutdown {
|
|
4
|
+
private isShuttingDown;
|
|
5
|
+
beforeApplicationShutdown(): void;
|
|
6
|
+
check(indicators: HealthIndicatorFunction[]): Promise<HealthCheckResult>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
import { Injectable } from "../container/index.js";
|
|
8
|
+
import { ServiceUnavailableException } from "../errors/http-exception.js";
|
|
9
|
+
export class HealthCheckService {
|
|
10
|
+
isShuttingDown = false;
|
|
11
|
+
beforeApplicationShutdown() {
|
|
12
|
+
this.isShuttingDown = true;
|
|
13
|
+
}
|
|
14
|
+
async check(indicators) {
|
|
15
|
+
if (this.isShuttingDown) {
|
|
16
|
+
const result = {
|
|
17
|
+
status: 'shutting_down',
|
|
18
|
+
info: {},
|
|
19
|
+
error: {},
|
|
20
|
+
details: {}
|
|
21
|
+
};
|
|
22
|
+
throw new ServiceUnavailableException('Service Unavailable', result);
|
|
23
|
+
}
|
|
24
|
+
const results = await Promise.allSettled(indicators.map((fn)=>fn()));
|
|
25
|
+
const info = {};
|
|
26
|
+
const error = {};
|
|
27
|
+
const details = {};
|
|
28
|
+
for (const result of results){
|
|
29
|
+
if (result.status === 'fulfilled') {
|
|
30
|
+
const indicatorResult = result.value;
|
|
31
|
+
for (const [key, value] of Object.entries(indicatorResult)){
|
|
32
|
+
details[key] = value;
|
|
33
|
+
if (value.status === 'up') {
|
|
34
|
+
info[key] = value;
|
|
35
|
+
} else {
|
|
36
|
+
error[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
const message = result.reason instanceof Error ? result.reason.message : 'Health check failed';
|
|
41
|
+
const key = 'unknown';
|
|
42
|
+
const value = {
|
|
43
|
+
status: 'down',
|
|
44
|
+
message
|
|
45
|
+
};
|
|
46
|
+
details[key] = value;
|
|
47
|
+
error[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const status = Object.keys(error).length > 0 ? 'error' : 'ok';
|
|
51
|
+
const checkResult = {
|
|
52
|
+
status,
|
|
53
|
+
info,
|
|
54
|
+
error,
|
|
55
|
+
details
|
|
56
|
+
};
|
|
57
|
+
if (status === 'error') {
|
|
58
|
+
throw new ServiceUnavailableException('Service Unavailable', checkResult);
|
|
59
|
+
}
|
|
60
|
+
return checkResult;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
HealthCheckService = _ts_decorate([
|
|
64
|
+
Injectable()
|
|
65
|
+
], HealthCheckService);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type HealthCheckStatus = 'ok' | 'error' | 'shutting_down';
|
|
2
|
+
export interface HealthIndicatorResult {
|
|
3
|
+
[key: string]: {
|
|
4
|
+
status: 'up' | 'down';
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export interface HealthCheckResult {
|
|
9
|
+
status: HealthCheckStatus;
|
|
10
|
+
info: HealthIndicatorResult;
|
|
11
|
+
error: HealthIndicatorResult;
|
|
12
|
+
details: HealthIndicatorResult;
|
|
13
|
+
}
|
|
14
|
+
export type HealthIndicatorFunction = () => Promise<HealthIndicatorResult>;
|
|
15
|
+
export type ResponseCheckCallback = (response: Response) => boolean | Promise<boolean>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { HealthModule } from './health.module';
|
|
2
|
+
export { HealthCheckService } from './health.service';
|
|
3
|
+
export { HealthIndicatorService } from './health.indicator';
|
|
4
|
+
export { HttpHealthIndicator } from './health.http';
|
|
5
|
+
export type { HealthCheckResult, HealthCheckStatus, HealthIndicatorResult, HealthIndicatorFunction, ResponseCheckCallback, } from './health.types';
|
|
6
|
+
export type { HttpPingOptions } from './health.http';
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export { EventEmitterModule, EventEmitter, EventEmitterSubscriber, OnEvent, ON_E
|
|
|
18
18
|
export type { EventHandler, OnEventMetadata } from './event-emitter/index';
|
|
19
19
|
export { ScheduleModule, ScheduleRegistry, ScheduleExecutor, Cron, Interval, SCHEDULE_MODULE_OPTIONS, CRON_METADATA, INTERVAL_METADATA, } from './schedule/index';
|
|
20
20
|
export type { RegisteredCronJob, RegisteredIntervalJob, CronMetadata, IntervalMetadata, ScheduleModuleOptions, } from './schedule/index';
|
|
21
|
+
export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthIndicator, } from './health/index';
|
|
22
|
+
export type { HealthCheckResult, HealthCheckStatus, HealthIndicatorResult, HealthIndicatorFunction, ResponseCheckCallback, HttpPingOptions, } from './health/index';
|
|
23
|
+
export { ThrottlerModule, ThrottlerGuard, ThrottlerStorage, Throttle, SkipThrottle, THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA, } from './throttler/index';
|
|
24
|
+
export type { ThrottlerModuleOptions, ThrottleConfig, ThrottlerStore, ThrottlerStorageRecord, RateLimitInfo, } from './throttler/index';
|
|
21
25
|
export { Module } from './module/index';
|
|
22
26
|
export type { ModuleOptions, DynamicModule } from './module/index';
|
|
23
27
|
export { UseMiddleware, UseGuards, UsePipes, UseInterceptors, UseFilters, Catch, SetMetadata, Reflector, APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE, } from './pipeline/index';
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,10 @@ export { CacheModule, CacheService, CacheInterceptor, MemoryCacheStore, CacheKey
|
|
|
21
21
|
export { EventEmitterModule, EventEmitter, EventEmitterSubscriber, OnEvent, ON_EVENT_METADATA } from "./event-emitter/index.js";
|
|
22
22
|
// Schedule
|
|
23
23
|
export { ScheduleModule, ScheduleRegistry, ScheduleExecutor, Cron, Interval, SCHEDULE_MODULE_OPTIONS, CRON_METADATA, INTERVAL_METADATA } from "./schedule/index.js";
|
|
24
|
+
// Health
|
|
25
|
+
export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthIndicator } from "./health/index.js";
|
|
26
|
+
// Throttler
|
|
27
|
+
export { ThrottlerModule, ThrottlerGuard, ThrottlerStorage, Throttle, SkipThrottle, THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA } from "./throttler/index.js";
|
|
24
28
|
// Module
|
|
25
29
|
export { Module } from "./module/index.js";
|
|
26
30
|
// Pipeline Decorators
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ThrottlerModule } from './throttler.module';
|
|
2
|
+
export { ThrottlerGuard } from './throttler.guard';
|
|
3
|
+
export { ThrottlerStorage } from './throttler.storage';
|
|
4
|
+
export { Throttle, SkipThrottle } from './throttler.decorators';
|
|
5
|
+
export { THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA } from './throttler.tokens';
|
|
6
|
+
export type { ThrottlerModuleOptions, ThrottleConfig, ThrottlerStore, ThrottlerStorageRecord, RateLimitInfo } from './throttler.types';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ThrottlerModule } from "./throttler.module.js";
|
|
2
|
+
export { ThrottlerGuard } from "./throttler.guard.js";
|
|
3
|
+
export { ThrottlerStorage } from "./throttler.storage.js";
|
|
4
|
+
export { Throttle, SkipThrottle } from "./throttler.decorators.js";
|
|
5
|
+
export { THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA } from "./throttler.tokens.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ThrottleConfig } from './throttler.types';
|
|
2
|
+
export declare const Throttle: (config: ThrottleConfig) => (target: Function | object, propertyKey?: string | symbol) => void;
|
|
3
|
+
export declare const SkipThrottle: () => (target: Function | object, propertyKey?: string | symbol) => void;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { SetMetadata } from "../pipeline/reflector.js";
|
|
2
|
+
import { THROTTLE_METADATA, SKIP_THROTTLE_METADATA } from "./throttler.tokens.js";
|
|
3
|
+
export const Throttle = (config)=>SetMetadata(THROTTLE_METADATA, config);
|
|
4
|
+
export const SkipThrottle = ()=>SetMetadata(SKIP_THROTTLE_METADATA, true);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CanActivate, ExecutionContext } from '../pipeline/types';
|
|
2
|
+
import type { ThrottlerModuleOptions, ThrottlerStore } from './throttler.types';
|
|
3
|
+
export declare class ThrottlerGuard implements CanActivate {
|
|
4
|
+
private options;
|
|
5
|
+
private storage;
|
|
6
|
+
private reflector;
|
|
7
|
+
constructor(options: ThrottlerModuleOptions, storage: ThrottlerStore);
|
|
8
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
import { Injectable, Inject } from "../container/decorators.js";
|
|
16
|
+
import { Reflector } from "../pipeline/reflector.js";
|
|
17
|
+
import { TooManyRequestsException } from "../errors/http-exception.js";
|
|
18
|
+
import { THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA } from "./throttler.tokens.js";
|
|
19
|
+
export class ThrottlerGuard {
|
|
20
|
+
options;
|
|
21
|
+
storage;
|
|
22
|
+
reflector = new Reflector();
|
|
23
|
+
constructor(options, storage){
|
|
24
|
+
this.options = options;
|
|
25
|
+
this.storage = storage;
|
|
26
|
+
}
|
|
27
|
+
async canActivate(context) {
|
|
28
|
+
const skip = this.reflector.getAllAndOverride(SKIP_THROTTLE_METADATA, context);
|
|
29
|
+
if (skip) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const override = this.reflector.getAllAndOverride(THROTTLE_METADATA, context);
|
|
33
|
+
const limit = override?.limit ?? this.options.limit;
|
|
34
|
+
const ttl = override?.ttl ?? this.options.ttl;
|
|
35
|
+
const request = context.getRequest();
|
|
36
|
+
const tracker = this.options.getTracker ? this.options.getTracker(request) : request.headers.get('x-forwarded-for') ?? 'anonymous';
|
|
37
|
+
const className = context.getClass().name;
|
|
38
|
+
const handlerName = String(context.getHandler());
|
|
39
|
+
const key = this.options.generateKey ? this.options.generateKey(tracker, {
|
|
40
|
+
className,
|
|
41
|
+
handlerName
|
|
42
|
+
}) : `throttler:${className}:${handlerName}:${tracker}`;
|
|
43
|
+
const { count, ttlMs } = await this.storage.increment(key, ttl);
|
|
44
|
+
const remaining = Math.max(0, limit - count);
|
|
45
|
+
const resetSeconds = Math.ceil(ttlMs / 1000);
|
|
46
|
+
const honoContext = context.getContext();
|
|
47
|
+
honoContext.header('X-RateLimit-Limit', String(limit));
|
|
48
|
+
honoContext.header('X-RateLimit-Remaining', String(remaining));
|
|
49
|
+
honoContext.header('X-RateLimit-Reset', String(resetSeconds));
|
|
50
|
+
const rateLimitInfo = {
|
|
51
|
+
limit,
|
|
52
|
+
remaining,
|
|
53
|
+
reset: resetSeconds
|
|
54
|
+
};
|
|
55
|
+
honoContext.set('rateLimit', rateLimitInfo);
|
|
56
|
+
if (count > limit) {
|
|
57
|
+
honoContext.header('Retry-After', String(resetSeconds));
|
|
58
|
+
throw new TooManyRequestsException();
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
ThrottlerGuard = _ts_decorate([
|
|
64
|
+
Injectable(),
|
|
65
|
+
_ts_param(0, Inject(THROTTLER_OPTIONS)),
|
|
66
|
+
_ts_param(1, Inject(THROTTLER_STORAGE)),
|
|
67
|
+
_ts_metadata("design:type", Function),
|
|
68
|
+
_ts_metadata("design:paramtypes", [
|
|
69
|
+
typeof ThrottlerModuleOptions === "undefined" ? Object : ThrottlerModuleOptions,
|
|
70
|
+
typeof ThrottlerStore === "undefined" ? Object : ThrottlerStore
|
|
71
|
+
])
|
|
72
|
+
], ThrottlerGuard);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { METADATA_KEYS } from "../constants.js";
|
|
2
|
+
import { defineMetadata } from "../metadata.js";
|
|
3
|
+
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
4
|
+
import { APP_GUARD } from "../pipeline/tokens.js";
|
|
5
|
+
import { ThrottlerGuard } from "./throttler.guard.js";
|
|
6
|
+
import { ThrottlerStorage } from "./throttler.storage.js";
|
|
7
|
+
import { THROTTLER_OPTIONS, THROTTLER_STORAGE } from "./throttler.tokens.js";
|
|
8
|
+
export class ThrottlerModule {
|
|
9
|
+
static forRoot(options) {
|
|
10
|
+
const moduleClass = class ThrottlerDynamicModule {
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(moduleClass, 'name', {
|
|
13
|
+
value: 'ThrottlerModule'
|
|
14
|
+
});
|
|
15
|
+
defineMetadata(METADATA_KEYS.MODULE, true, moduleClass);
|
|
16
|
+
MetadataRegistry.setModuleOptions(moduleClass, {
|
|
17
|
+
exports: [
|
|
18
|
+
THROTTLER_OPTIONS,
|
|
19
|
+
THROTTLER_STORAGE,
|
|
20
|
+
ThrottlerGuard
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
const providers = [
|
|
24
|
+
{
|
|
25
|
+
token: THROTTLER_OPTIONS,
|
|
26
|
+
useValue: options
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
token: THROTTLER_STORAGE,
|
|
30
|
+
useValue: options.storage ?? new ThrottlerStorage()
|
|
31
|
+
},
|
|
32
|
+
ThrottlerGuard,
|
|
33
|
+
{
|
|
34
|
+
token: APP_GUARD,
|
|
35
|
+
useExisting: ThrottlerGuard
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
return {
|
|
39
|
+
module: moduleClass,
|
|
40
|
+
providers
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ThrottlerStore, ThrottlerStorageRecord } from './throttler.types';
|
|
2
|
+
export declare class ThrottlerStorage implements ThrottlerStore {
|
|
3
|
+
private current;
|
|
4
|
+
private previous;
|
|
5
|
+
private lastSwap;
|
|
6
|
+
private readonly swapIntervalMs;
|
|
7
|
+
increment(key: string, ttlMs: number): ThrottlerStorageRecord;
|
|
8
|
+
reset(key: string): void;
|
|
9
|
+
private maybeSwap;
|
|
10
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
import { Injectable } from "../container/decorators.js";
|
|
8
|
+
export class ThrottlerStorage {
|
|
9
|
+
current = new Map();
|
|
10
|
+
previous = new Map();
|
|
11
|
+
lastSwap = Date.now();
|
|
12
|
+
swapIntervalMs = 60_000;
|
|
13
|
+
increment(key, ttlMs) {
|
|
14
|
+
this.maybeSwap();
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
let entry = this.current.get(key) ?? this.previous.get(key);
|
|
17
|
+
if (!entry || now >= entry.resetTime) {
|
|
18
|
+
entry = {
|
|
19
|
+
count: 0,
|
|
20
|
+
resetTime: now + ttlMs
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
entry.count++;
|
|
24
|
+
this.current.set(key, entry);
|
|
25
|
+
return {
|
|
26
|
+
count: entry.count,
|
|
27
|
+
ttlMs: entry.resetTime - now
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
reset(key) {
|
|
31
|
+
this.current.delete(key);
|
|
32
|
+
this.previous.delete(key);
|
|
33
|
+
}
|
|
34
|
+
maybeSwap() {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
if (now - this.lastSwap < this.swapIntervalMs) return;
|
|
37
|
+
this.previous = this.current;
|
|
38
|
+
this.current = new Map();
|
|
39
|
+
this.lastSwap = now;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
ThrottlerStorage = _ts_decorate([
|
|
43
|
+
Injectable()
|
|
44
|
+
], ThrottlerStorage);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { InjectionToken } from '../container/types';
|
|
2
|
+
import type { ThrottlerModuleOptions, ThrottlerStore } from './throttler.types';
|
|
3
|
+
export declare const THROTTLER_OPTIONS: InjectionToken<ThrottlerModuleOptions>;
|
|
4
|
+
export declare const THROTTLER_STORAGE: InjectionToken<ThrottlerStore>;
|
|
5
|
+
export declare const THROTTLE_METADATA = "vela:throttle";
|
|
6
|
+
export declare const SKIP_THROTTLE_METADATA = "vela:skip-throttle";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { InjectionToken } from "../container/types.js";
|
|
2
|
+
export const THROTTLER_OPTIONS = new InjectionToken('THROTTLER_OPTIONS');
|
|
3
|
+
export const THROTTLER_STORAGE = new InjectionToken('THROTTLER_STORAGE');
|
|
4
|
+
export const THROTTLE_METADATA = 'vela:throttle';
|
|
5
|
+
export const SKIP_THROTTLE_METADATA = 'vela:skip-throttle';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ThrottleConfig {
|
|
2
|
+
limit: number;
|
|
3
|
+
ttl: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ThrottlerStorageRecord {
|
|
6
|
+
count: number;
|
|
7
|
+
ttlMs: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ThrottlerStore {
|
|
10
|
+
increment(key: string, ttlMs: number): ThrottlerStorageRecord | Promise<ThrottlerStorageRecord>;
|
|
11
|
+
reset(key: string): void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface RateLimitInfo {
|
|
14
|
+
limit: number;
|
|
15
|
+
remaining: number;
|
|
16
|
+
reset: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ThrottlerModuleOptions extends ThrottleConfig {
|
|
19
|
+
storage?: ThrottlerStore;
|
|
20
|
+
getTracker?: (request: Request) => string;
|
|
21
|
+
generateKey?: (tracker: string, context: {
|
|
22
|
+
className: string;
|
|
23
|
+
handlerName: string;
|
|
24
|
+
}) => string;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|