te.js 2.1.0 → 2.1.2
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.md +197 -196
- package/auto-docs/analysis/handler-analyzer.js +58 -58
- package/auto-docs/analysis/source-resolver.js +101 -101
- package/auto-docs/constants.js +37 -37
- package/auto-docs/docs-llm/index.js +7 -7
- package/auto-docs/docs-llm/prompts.js +222 -222
- package/auto-docs/docs-llm/provider.js +132 -132
- package/auto-docs/index.js +146 -146
- package/auto-docs/openapi/endpoint-processor.js +277 -277
- package/auto-docs/openapi/generator.js +107 -107
- package/auto-docs/openapi/level3.js +131 -131
- package/auto-docs/openapi/spec-builders.js +244 -244
- package/auto-docs/ui/docs-ui.js +186 -186
- package/auto-docs/utils/logger.js +17 -17
- package/auto-docs/utils/strip-usage.js +10 -10
- package/cli/docs-command.js +315 -315
- package/cli/fly-command.js +71 -71
- package/cli/index.js +56 -56
- package/cors/index.js +71 -0
- package/database/index.js +165 -165
- package/database/mongodb.js +146 -146
- package/database/redis.js +201 -201
- package/docs/README.md +36 -36
- package/docs/ammo.md +362 -362
- package/docs/api-reference.md +490 -490
- package/docs/auto-docs.md +216 -216
- package/docs/cli.md +152 -152
- package/docs/configuration.md +275 -275
- package/docs/database.md +390 -390
- package/docs/error-handling.md +438 -438
- package/docs/file-uploads.md +333 -333
- package/docs/getting-started.md +214 -214
- package/docs/middleware.md +355 -355
- package/docs/rate-limiting.md +393 -393
- package/docs/routing.md +302 -302
- package/lib/llm/client.js +73 -0
- package/lib/llm/index.js +7 -0
- package/lib/llm/parse.js +89 -0
- package/package.json +64 -62
- package/rate-limit/algorithms/fixed-window.js +141 -141
- package/rate-limit/algorithms/sliding-window.js +147 -147
- package/rate-limit/algorithms/token-bucket.js +115 -115
- package/rate-limit/base.js +165 -165
- package/rate-limit/index.js +147 -147
- package/rate-limit/storage/base.js +104 -104
- package/rate-limit/storage/memory.js +101 -101
- package/rate-limit/storage/redis.js +88 -88
- package/server/ammo/body-parser.js +220 -220
- package/server/ammo/dispatch-helper.js +103 -103
- package/server/ammo/enhancer.js +57 -57
- package/server/ammo.js +454 -415
- package/server/endpoint.js +97 -74
- package/server/error.js +9 -9
- package/server/errors/code-context.js +125 -125
- package/server/errors/llm-error-service.js +140 -140
- package/server/files/helper.js +33 -33
- package/server/files/uploader.js +143 -143
- package/server/handler.js +158 -119
- package/server/target.js +185 -175
- package/server/targets/middleware-validator.js +22 -22
- package/server/targets/path-validator.js +21 -21
- package/server/targets/registry.js +160 -160
- package/server/targets/shoot-validator.js +21 -21
- package/te.js +428 -402
- package/utils/auto-register.js +17 -17
- package/utils/configuration.js +64 -64
- package/utils/errors-llm-config.js +84 -84
- package/utils/request-logger.js +43 -43
- package/utils/status-codes.js +82 -82
- package/utils/tejas-entrypoint-html.js +18 -18
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
import RateLimiter from '../base.js';
|
|
2
|
-
import TejError from '../../server/error.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Sliding Window Rate Limiter Implementation
|
|
6
|
-
*
|
|
7
|
-
* @extends RateLimiter
|
|
8
|
-
* @description
|
|
9
|
-
* The sliding window algorithm provides precise rate limiting by considering both the current
|
|
10
|
-
* and previous time windows with configurable weights. This prevents traffic spikes that can
|
|
11
|
-
* occur at window boundaries with fixed window rate limiting.
|
|
12
|
-
*
|
|
13
|
-
* Key features:
|
|
14
|
-
* - Smoother rate limiting than fixed windows
|
|
15
|
-
* - Prevents boundary spike issues
|
|
16
|
-
* - Configurable time granularity
|
|
17
|
-
* - Weighted window transitions
|
|
18
|
-
*
|
|
19
|
-
* For example, with 60-second windows and 30 seconds into the current window:
|
|
20
|
-
* - Current window gets full weight (e.g. 1.0)
|
|
21
|
-
* - Previous window gets partial weight (e.g. 0.5)
|
|
22
|
-
* Total requests = (current_requests * 1.0) + (previous_requests * 0.5)
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* // Create a sliding window rate limiter with weighted windows
|
|
26
|
-
* const limiter = new SlidingWindowRateLimiter({
|
|
27
|
-
* maxRequests: 100, // Allow 100 requests per minute
|
|
28
|
-
* timeWindowSeconds: 60,
|
|
29
|
-
* slidingWindow: {
|
|
30
|
-
* granularity: 1, // 1-second precision
|
|
31
|
-
* weights: {
|
|
32
|
-
* current: 1, // Full weight for current window
|
|
33
|
-
* previous: 0.5 // Half weight for previous window
|
|
34
|
-
* }
|
|
35
|
-
* }
|
|
36
|
-
* });
|
|
37
|
-
*
|
|
38
|
-
* // Use in an API endpoint
|
|
39
|
-
* async function handleRequest(ip) {
|
|
40
|
-
* const result = await limiter.consume(ip);
|
|
41
|
-
* if (!result.success) {
|
|
42
|
-
* throw new TejError(429, 'Rate limit exceeded');
|
|
43
|
-
* }
|
|
44
|
-
* // Process request...
|
|
45
|
-
* }
|
|
46
|
-
*/
|
|
47
|
-
class SlidingWindowRateLimiter extends RateLimiter {
|
|
48
|
-
constructor(options) {
|
|
49
|
-
if (!options.slidingWindowConfig) {
|
|
50
|
-
options.slidingWindowConfig = {}; // Ensure defaults are set in base class
|
|
51
|
-
}
|
|
52
|
-
super(options);
|
|
53
|
-
|
|
54
|
-
if (!this.slidingWindowOptions) {
|
|
55
|
-
throw new TejError(
|
|
56
|
-
400,
|
|
57
|
-
'SlidingWindowRateLimiter requires slidingWindowConfig options',
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Check if a request should be allowed based on weighted window counts
|
|
64
|
-
*
|
|
65
|
-
* @param {string} identifier - Unique identifier for rate limiting (e.g., IP address, user ID)
|
|
66
|
-
* @returns {Promise<Object>} Rate limit check result
|
|
67
|
-
* @returns {boolean} result.success - Whether the request is allowed
|
|
68
|
-
* @returns {number} result.remainingRequests - Number of requests remaining in the current window
|
|
69
|
-
* @returns {number} result.resetTime - Unix timestamp when the current window ends
|
|
70
|
-
*/
|
|
71
|
-
async consume(identifier) {
|
|
72
|
-
const key = this.getKey(identifier);
|
|
73
|
-
const now = Date.now();
|
|
74
|
-
const options = this.getAlgorithmOptions('sliding-window');
|
|
75
|
-
|
|
76
|
-
const stored = await this.storage.get(key);
|
|
77
|
-
if (!stored) {
|
|
78
|
-
await this.storage.set(
|
|
79
|
-
key,
|
|
80
|
-
{
|
|
81
|
-
counter: 1,
|
|
82
|
-
timestamps: [now],
|
|
83
|
-
windowStart:
|
|
84
|
-
Math.floor(now / (options.granularity * 1000)) *
|
|
85
|
-
(options.granularity * 1000),
|
|
86
|
-
},
|
|
87
|
-
this.options.timeWindowSeconds,
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
success: true,
|
|
92
|
-
remainingRequests: this.options.maxRequests - 1,
|
|
93
|
-
resetTime: Math.floor(now / 1000) + this.options.timeWindowSeconds,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Calculate window boundaries
|
|
98
|
-
const currentWindowStart =
|
|
99
|
-
Math.floor(now / (options.granularity * 1000)) *
|
|
100
|
-
(options.granularity * 1000);
|
|
101
|
-
const previousWindowStart =
|
|
102
|
-
currentWindowStart - this.options.timeWindowSeconds * 1000;
|
|
103
|
-
|
|
104
|
-
// Split timestamps into current and previous windows
|
|
105
|
-
const currentWindowRequests = stored.timestamps.filter(
|
|
106
|
-
(ts) => ts >= currentWindowStart,
|
|
107
|
-
).length;
|
|
108
|
-
const previousWindowRequests = stored.timestamps.filter(
|
|
109
|
-
(ts) => ts >= previousWindowStart && ts < currentWindowStart,
|
|
110
|
-
).length;
|
|
111
|
-
|
|
112
|
-
// Calculate weighted request count
|
|
113
|
-
const weightedCount =
|
|
114
|
-
currentWindowRequests * options.weights.current +
|
|
115
|
-
previousWindowRequests * options.weights.previous;
|
|
116
|
-
|
|
117
|
-
if (weightedCount >= this.options.maxRequests) {
|
|
118
|
-
return {
|
|
119
|
-
success: false,
|
|
120
|
-
remainingRequests: 0,
|
|
121
|
-
resetTime: Math.floor(
|
|
122
|
-
currentWindowStart / 1000 + this.options.timeWindowSeconds,
|
|
123
|
-
),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Remove timestamps outside both windows and add new timestamp
|
|
128
|
-
stored.timestamps = stored.timestamps.filter(
|
|
129
|
-
(ts) => ts >= previousWindowStart,
|
|
130
|
-
);
|
|
131
|
-
stored.timestamps.push(now);
|
|
132
|
-
stored.windowStart = currentWindowStart;
|
|
133
|
-
await this.storage.set(key, stored, this.options.timeWindowSeconds);
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
success: true,
|
|
137
|
-
remainingRequests: Math.floor(
|
|
138
|
-
this.options.maxRequests - weightedCount - 1,
|
|
139
|
-
),
|
|
140
|
-
resetTime: Math.floor(
|
|
141
|
-
currentWindowStart / 1000 + this.options.timeWindowSeconds,
|
|
142
|
-
),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export default SlidingWindowRateLimiter;
|
|
1
|
+
import RateLimiter from '../base.js';
|
|
2
|
+
import TejError from '../../server/error.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sliding Window Rate Limiter Implementation
|
|
6
|
+
*
|
|
7
|
+
* @extends RateLimiter
|
|
8
|
+
* @description
|
|
9
|
+
* The sliding window algorithm provides precise rate limiting by considering both the current
|
|
10
|
+
* and previous time windows with configurable weights. This prevents traffic spikes that can
|
|
11
|
+
* occur at window boundaries with fixed window rate limiting.
|
|
12
|
+
*
|
|
13
|
+
* Key features:
|
|
14
|
+
* - Smoother rate limiting than fixed windows
|
|
15
|
+
* - Prevents boundary spike issues
|
|
16
|
+
* - Configurable time granularity
|
|
17
|
+
* - Weighted window transitions
|
|
18
|
+
*
|
|
19
|
+
* For example, with 60-second windows and 30 seconds into the current window:
|
|
20
|
+
* - Current window gets full weight (e.g. 1.0)
|
|
21
|
+
* - Previous window gets partial weight (e.g. 0.5)
|
|
22
|
+
* Total requests = (current_requests * 1.0) + (previous_requests * 0.5)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Create a sliding window rate limiter with weighted windows
|
|
26
|
+
* const limiter = new SlidingWindowRateLimiter({
|
|
27
|
+
* maxRequests: 100, // Allow 100 requests per minute
|
|
28
|
+
* timeWindowSeconds: 60,
|
|
29
|
+
* slidingWindow: {
|
|
30
|
+
* granularity: 1, // 1-second precision
|
|
31
|
+
* weights: {
|
|
32
|
+
* current: 1, // Full weight for current window
|
|
33
|
+
* previous: 0.5 // Half weight for previous window
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* // Use in an API endpoint
|
|
39
|
+
* async function handleRequest(ip) {
|
|
40
|
+
* const result = await limiter.consume(ip);
|
|
41
|
+
* if (!result.success) {
|
|
42
|
+
* throw new TejError(429, 'Rate limit exceeded');
|
|
43
|
+
* }
|
|
44
|
+
* // Process request...
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
class SlidingWindowRateLimiter extends RateLimiter {
|
|
48
|
+
constructor(options) {
|
|
49
|
+
if (!options.slidingWindowConfig) {
|
|
50
|
+
options.slidingWindowConfig = {}; // Ensure defaults are set in base class
|
|
51
|
+
}
|
|
52
|
+
super(options);
|
|
53
|
+
|
|
54
|
+
if (!this.slidingWindowOptions) {
|
|
55
|
+
throw new TejError(
|
|
56
|
+
400,
|
|
57
|
+
'SlidingWindowRateLimiter requires slidingWindowConfig options',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a request should be allowed based on weighted window counts
|
|
64
|
+
*
|
|
65
|
+
* @param {string} identifier - Unique identifier for rate limiting (e.g., IP address, user ID)
|
|
66
|
+
* @returns {Promise<Object>} Rate limit check result
|
|
67
|
+
* @returns {boolean} result.success - Whether the request is allowed
|
|
68
|
+
* @returns {number} result.remainingRequests - Number of requests remaining in the current window
|
|
69
|
+
* @returns {number} result.resetTime - Unix timestamp when the current window ends
|
|
70
|
+
*/
|
|
71
|
+
async consume(identifier) {
|
|
72
|
+
const key = this.getKey(identifier);
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const options = this.getAlgorithmOptions('sliding-window');
|
|
75
|
+
|
|
76
|
+
const stored = await this.storage.get(key);
|
|
77
|
+
if (!stored) {
|
|
78
|
+
await this.storage.set(
|
|
79
|
+
key,
|
|
80
|
+
{
|
|
81
|
+
counter: 1,
|
|
82
|
+
timestamps: [now],
|
|
83
|
+
windowStart:
|
|
84
|
+
Math.floor(now / (options.granularity * 1000)) *
|
|
85
|
+
(options.granularity * 1000),
|
|
86
|
+
},
|
|
87
|
+
this.options.timeWindowSeconds,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
remainingRequests: this.options.maxRequests - 1,
|
|
93
|
+
resetTime: Math.floor(now / 1000) + this.options.timeWindowSeconds,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate window boundaries
|
|
98
|
+
const currentWindowStart =
|
|
99
|
+
Math.floor(now / (options.granularity * 1000)) *
|
|
100
|
+
(options.granularity * 1000);
|
|
101
|
+
const previousWindowStart =
|
|
102
|
+
currentWindowStart - this.options.timeWindowSeconds * 1000;
|
|
103
|
+
|
|
104
|
+
// Split timestamps into current and previous windows
|
|
105
|
+
const currentWindowRequests = stored.timestamps.filter(
|
|
106
|
+
(ts) => ts >= currentWindowStart,
|
|
107
|
+
).length;
|
|
108
|
+
const previousWindowRequests = stored.timestamps.filter(
|
|
109
|
+
(ts) => ts >= previousWindowStart && ts < currentWindowStart,
|
|
110
|
+
).length;
|
|
111
|
+
|
|
112
|
+
// Calculate weighted request count
|
|
113
|
+
const weightedCount =
|
|
114
|
+
currentWindowRequests * options.weights.current +
|
|
115
|
+
previousWindowRequests * options.weights.previous;
|
|
116
|
+
|
|
117
|
+
if (weightedCount >= this.options.maxRequests) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
remainingRequests: 0,
|
|
121
|
+
resetTime: Math.floor(
|
|
122
|
+
currentWindowStart / 1000 + this.options.timeWindowSeconds,
|
|
123
|
+
),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Remove timestamps outside both windows and add new timestamp
|
|
128
|
+
stored.timestamps = stored.timestamps.filter(
|
|
129
|
+
(ts) => ts >= previousWindowStart,
|
|
130
|
+
);
|
|
131
|
+
stored.timestamps.push(now);
|
|
132
|
+
stored.windowStart = currentWindowStart;
|
|
133
|
+
await this.storage.set(key, stored, this.options.timeWindowSeconds);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
remainingRequests: Math.floor(
|
|
138
|
+
this.options.maxRequests - weightedCount - 1,
|
|
139
|
+
),
|
|
140
|
+
resetTime: Math.floor(
|
|
141
|
+
currentWindowStart / 1000 + this.options.timeWindowSeconds,
|
|
142
|
+
),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default SlidingWindowRateLimiter;
|
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
import RateLimiter from '../base.js';
|
|
2
|
-
import TejError from '../../server/error.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Token Bucket Rate Limiter Implementation
|
|
6
|
-
*
|
|
7
|
-
* @extends RateLimiter
|
|
8
|
-
* @description
|
|
9
|
-
* The token bucket algorithm provides smooth rate limiting with burst handling capabilities.
|
|
10
|
-
* It maintains a bucket of tokens that refills at a constant rate, allowing for temporary
|
|
11
|
-
* bursts of traffic while maintaining a long-term average rate.
|
|
12
|
-
*
|
|
13
|
-
* Key features:
|
|
14
|
-
* - Smooth rate limiting with continuous token regeneration
|
|
15
|
-
* - Burst handling with configurable burst size
|
|
16
|
-
* - Predictable long-term average rate
|
|
17
|
-
* - Fair distribution of requests over time
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* // Create a rate limiter that allows bursts
|
|
21
|
-
* const limiter = new TokenBucketRateLimiter({
|
|
22
|
-
* maxRequests: 60, // Base rate: 60 requests per minute
|
|
23
|
-
* timeWindowSeconds: 60,
|
|
24
|
-
* tokenBucket: {
|
|
25
|
-
* refillRate: 1, // Refill 1 token per second
|
|
26
|
-
* burstSize: 90 // Allow bursts up to 90 requests
|
|
27
|
-
* }
|
|
28
|
-
* });
|
|
29
|
-
*
|
|
30
|
-
* // Use the rate limiter in an endpoint
|
|
31
|
-
* async function handleRequest(ip) {
|
|
32
|
-
* const result = await limiter.consume(ip);
|
|
33
|
-
* if (!result.success) {
|
|
34
|
-
* throw new TejError(429, 'Rate limit exceeded');
|
|
35
|
-
* }
|
|
36
|
-
* // Handle request...
|
|
37
|
-
* }
|
|
38
|
-
*/
|
|
39
|
-
class TokenBucketRateLimiter extends RateLimiter {
|
|
40
|
-
constructor(options) {
|
|
41
|
-
if (!options.tokenBucketConfig) {
|
|
42
|
-
options.tokenBucketConfig = {}; // Ensure defaults are set in base class
|
|
43
|
-
}
|
|
44
|
-
super(options);
|
|
45
|
-
|
|
46
|
-
if (!this.tokenBucketOptions) {
|
|
47
|
-
throw new TejError(
|
|
48
|
-
400,
|
|
49
|
-
'TokenBucketRateLimiter requires tokenBucketConfig options',
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Check if a request should be allowed and update token count
|
|
56
|
-
*
|
|
57
|
-
* @param {string} identifier - Unique identifier for rate limiting (e.g., IP address, user ID)
|
|
58
|
-
* @returns {Promise<Object>} Rate limit check result
|
|
59
|
-
* @returns {boolean} result.success - Whether the request is allowed
|
|
60
|
-
* @returns {number} result.remainingRequests - Number of tokens remaining in the bucket
|
|
61
|
-
* @returns {number} result.resetTime - Unix timestamp when the next token will be added
|
|
62
|
-
*/
|
|
63
|
-
async consume(identifier) {
|
|
64
|
-
const key = this.getKey(identifier);
|
|
65
|
-
const now = Date.now();
|
|
66
|
-
const options = this.getAlgorithmOptions('tokenBucketConfig');
|
|
67
|
-
|
|
68
|
-
const stored = await this.storage.get(key);
|
|
69
|
-
if (!stored) {
|
|
70
|
-
await this.storage.set(
|
|
71
|
-
key,
|
|
72
|
-
{
|
|
73
|
-
tokens: options.burstSize - 1,
|
|
74
|
-
lastRefill: now,
|
|
75
|
-
},
|
|
76
|
-
this.options.timeWindowSeconds,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
success: true,
|
|
81
|
-
remainingRequests: options.burstSize - 1,
|
|
82
|
-
resetTime: Math.floor(now / 1000) + this.options.timeWindowSeconds,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Calculate token refill based on configured refill rate
|
|
87
|
-
const timePassed = now - stored.lastRefill;
|
|
88
|
-
const refillTokens = Math.floor((timePassed * options.refillRate) / 1000);
|
|
89
|
-
|
|
90
|
-
stored.tokens = Math.min(options.burstSize, stored.tokens + refillTokens);
|
|
91
|
-
stored.lastRefill = now;
|
|
92
|
-
|
|
93
|
-
if (stored.tokens < 1) {
|
|
94
|
-
const timeToNextToken = Math.ceil(
|
|
95
|
-
((1 - stored.tokens) / options.refillRate) * 1000,
|
|
96
|
-
);
|
|
97
|
-
return {
|
|
98
|
-
success: false,
|
|
99
|
-
remainingRequests: 0,
|
|
100
|
-
resetTime: Math.floor((now + timeToNextToken) / 1000),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
stored.tokens--;
|
|
105
|
-
await this.storage.set(key, stored, this.options.timeWindowSeconds);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
success: true,
|
|
109
|
-
remainingRequests: Math.floor(stored.tokens),
|
|
110
|
-
resetTime: Math.floor(now / 1000) + this.options.timeWindowSeconds,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export default TokenBucketRateLimiter;
|
|
1
|
+
import RateLimiter from '../base.js';
|
|
2
|
+
import TejError from '../../server/error.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Token Bucket Rate Limiter Implementation
|
|
6
|
+
*
|
|
7
|
+
* @extends RateLimiter
|
|
8
|
+
* @description
|
|
9
|
+
* The token bucket algorithm provides smooth rate limiting with burst handling capabilities.
|
|
10
|
+
* It maintains a bucket of tokens that refills at a constant rate, allowing for temporary
|
|
11
|
+
* bursts of traffic while maintaining a long-term average rate.
|
|
12
|
+
*
|
|
13
|
+
* Key features:
|
|
14
|
+
* - Smooth rate limiting with continuous token regeneration
|
|
15
|
+
* - Burst handling with configurable burst size
|
|
16
|
+
* - Predictable long-term average rate
|
|
17
|
+
* - Fair distribution of requests over time
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Create a rate limiter that allows bursts
|
|
21
|
+
* const limiter = new TokenBucketRateLimiter({
|
|
22
|
+
* maxRequests: 60, // Base rate: 60 requests per minute
|
|
23
|
+
* timeWindowSeconds: 60,
|
|
24
|
+
* tokenBucket: {
|
|
25
|
+
* refillRate: 1, // Refill 1 token per second
|
|
26
|
+
* burstSize: 90 // Allow bursts up to 90 requests
|
|
27
|
+
* }
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // Use the rate limiter in an endpoint
|
|
31
|
+
* async function handleRequest(ip) {
|
|
32
|
+
* const result = await limiter.consume(ip);
|
|
33
|
+
* if (!result.success) {
|
|
34
|
+
* throw new TejError(429, 'Rate limit exceeded');
|
|
35
|
+
* }
|
|
36
|
+
* // Handle request...
|
|
37
|
+
* }
|
|
38
|
+
*/
|
|
39
|
+
class TokenBucketRateLimiter extends RateLimiter {
|
|
40
|
+
constructor(options) {
|
|
41
|
+
if (!options.tokenBucketConfig) {
|
|
42
|
+
options.tokenBucketConfig = {}; // Ensure defaults are set in base class
|
|
43
|
+
}
|
|
44
|
+
super(options);
|
|
45
|
+
|
|
46
|
+
if (!this.tokenBucketOptions) {
|
|
47
|
+
throw new TejError(
|
|
48
|
+
400,
|
|
49
|
+
'TokenBucketRateLimiter requires tokenBucketConfig options',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a request should be allowed and update token count
|
|
56
|
+
*
|
|
57
|
+
* @param {string} identifier - Unique identifier for rate limiting (e.g., IP address, user ID)
|
|
58
|
+
* @returns {Promise<Object>} Rate limit check result
|
|
59
|
+
* @returns {boolean} result.success - Whether the request is allowed
|
|
60
|
+
* @returns {number} result.remainingRequests - Number of tokens remaining in the bucket
|
|
61
|
+
* @returns {number} result.resetTime - Unix timestamp when the next token will be added
|
|
62
|
+
*/
|
|
63
|
+
async consume(identifier) {
|
|
64
|
+
const key = this.getKey(identifier);
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const options = this.getAlgorithmOptions('tokenBucketConfig');
|
|
67
|
+
|
|
68
|
+
const stored = await this.storage.get(key);
|
|
69
|
+
if (!stored) {
|
|
70
|
+
await this.storage.set(
|
|
71
|
+
key,
|
|
72
|
+
{
|
|
73
|
+
tokens: options.burstSize - 1,
|
|
74
|
+
lastRefill: now,
|
|
75
|
+
},
|
|
76
|
+
this.options.timeWindowSeconds,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
remainingRequests: options.burstSize - 1,
|
|
82
|
+
resetTime: Math.floor(now / 1000) + this.options.timeWindowSeconds,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Calculate token refill based on configured refill rate
|
|
87
|
+
const timePassed = now - stored.lastRefill;
|
|
88
|
+
const refillTokens = Math.floor((timePassed * options.refillRate) / 1000);
|
|
89
|
+
|
|
90
|
+
stored.tokens = Math.min(options.burstSize, stored.tokens + refillTokens);
|
|
91
|
+
stored.lastRefill = now;
|
|
92
|
+
|
|
93
|
+
if (stored.tokens < 1) {
|
|
94
|
+
const timeToNextToken = Math.ceil(
|
|
95
|
+
((1 - stored.tokens) / options.refillRate) * 1000,
|
|
96
|
+
);
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
remainingRequests: 0,
|
|
100
|
+
resetTime: Math.floor((now + timeToNextToken) / 1000),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
stored.tokens--;
|
|
105
|
+
await this.storage.set(key, stored, this.options.timeWindowSeconds);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
remainingRequests: Math.floor(stored.tokens),
|
|
110
|
+
resetTime: Math.floor(now / 1000) + this.options.timeWindowSeconds,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default TokenBucketRateLimiter;
|