navis.js 3.0.2 → 4.0.0
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 +66 -1
- package/examples/lambda-optimized.js +103 -0
- package/examples/lambda.js +30 -29
- package/examples/v4-features-demo.js +171 -0
- package/package.json +1 -1
- package/src/auth/authenticator.js +223 -0
- package/src/core/advanced-router.js +186 -0
- package/src/core/app.js +255 -176
- package/src/core/lambda-handler.js +130 -0
- package/src/errors/error-handler.js +157 -0
- package/src/index.js +62 -0
- package/src/middleware/cold-start-tracker.js +56 -0
- package/src/middleware/rate-limiter.js +159 -0
- package/src/utils/lazy-init.js +100 -0
- package/src/utils/service-client-pool.js +131 -0
- package/src/validation/validator.js +301 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optimized Lambda Handler
|
|
3
|
+
* v3.1: Enhanced Lambda handler with cold start optimizations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class LambdaHandler {
|
|
7
|
+
constructor(app, options = {}) {
|
|
8
|
+
this.app = app;
|
|
9
|
+
this.isWarm = false;
|
|
10
|
+
this.initTime = Date.now();
|
|
11
|
+
this.invocationCount = 0;
|
|
12
|
+
this.coldStartCount = 0;
|
|
13
|
+
this.enableMetrics = options.enableMetrics !== false;
|
|
14
|
+
this.warmupPath = options.warmupPath || '/warmup';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handle Lambda invocation
|
|
19
|
+
* @param {Object} event - Lambda event
|
|
20
|
+
* @param {Object} context - Lambda context
|
|
21
|
+
* @returns {Promise<Object>} - Lambda response
|
|
22
|
+
*/
|
|
23
|
+
async handle(event, context) {
|
|
24
|
+
this.invocationCount++;
|
|
25
|
+
|
|
26
|
+
// Detect warm-up events
|
|
27
|
+
if (this.isWarmupEvent(event)) {
|
|
28
|
+
return this.handleWarmup();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Track cold start
|
|
32
|
+
const isColdStart = !this.isWarm;
|
|
33
|
+
if (isColdStart) {
|
|
34
|
+
this.isWarm = true;
|
|
35
|
+
this.coldStartCount++;
|
|
36
|
+
|
|
37
|
+
if (this.enableMetrics) {
|
|
38
|
+
const coldStartDuration = Date.now() - this.initTime;
|
|
39
|
+
console.log(JSON.stringify({
|
|
40
|
+
type: 'cold-start',
|
|
41
|
+
duration: coldStartDuration,
|
|
42
|
+
memoryLimit: context.memoryLimitInMB,
|
|
43
|
+
requestId: context.requestId,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Add cold start headers to response
|
|
49
|
+
const response = await this.app.handleLambda(event);
|
|
50
|
+
|
|
51
|
+
if (isColdStart && response.headers) {
|
|
52
|
+
response.headers['X-Cold-Start'] = 'true';
|
|
53
|
+
response.headers['X-Init-Time'] = (Date.now() - this.initTime).toString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if event is a warm-up event
|
|
61
|
+
* @param {Object} event - Lambda event
|
|
62
|
+
* @returns {boolean} - True if warm-up event
|
|
63
|
+
*/
|
|
64
|
+
isWarmupEvent(event) {
|
|
65
|
+
// Check various warm-up event formats
|
|
66
|
+
if (event.source === 'serverless-plugin-warmup') {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (event.warmup === true || event['serverless-plugin-warmup']) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if it's a warm-up HTTP request
|
|
75
|
+
if (event.httpMethod === 'GET' &&
|
|
76
|
+
(event.path === this.warmupPath || event.rawPath === this.warmupPath)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handle warm-up event
|
|
85
|
+
* @returns {Object} - Warm-up response
|
|
86
|
+
*/
|
|
87
|
+
handleWarmup() {
|
|
88
|
+
// Mark as warm
|
|
89
|
+
this.isWarm = true;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
statusCode: 200,
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
status: 'warmed',
|
|
98
|
+
invocationCount: this.invocationCount,
|
|
99
|
+
coldStartCount: this.coldStartCount,
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get handler statistics
|
|
106
|
+
* @returns {Object} - Handler statistics
|
|
107
|
+
*/
|
|
108
|
+
getStats() {
|
|
109
|
+
return {
|
|
110
|
+
isWarm: this.isWarm,
|
|
111
|
+
invocationCount: this.invocationCount,
|
|
112
|
+
coldStartCount: this.coldStartCount,
|
|
113
|
+
initTime: this.initTime,
|
|
114
|
+
uptime: Date.now() - this.initTime,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Reset handler state (useful for testing)
|
|
120
|
+
*/
|
|
121
|
+
reset() {
|
|
122
|
+
this.isWarm = false;
|
|
123
|
+
this.invocationCount = 0;
|
|
124
|
+
this.coldStartCount = 0;
|
|
125
|
+
this.initTime = Date.now();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = LambdaHandler;
|
|
130
|
+
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Error Handling
|
|
3
|
+
* v4: Custom error classes and error handling middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class AppError extends Error {
|
|
7
|
+
constructor(message, statusCode = 500, code = null) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = this.constructor.name;
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
this.code = code || this.constructor.name.toUpperCase();
|
|
12
|
+
this.isOperational = true;
|
|
13
|
+
Error.captureStackTrace(this, this.constructor);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class NotFoundError extends AppError {
|
|
18
|
+
constructor(message = 'Resource not found') {
|
|
19
|
+
super(message, 404, 'NOT_FOUND');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class BadRequestError extends AppError {
|
|
24
|
+
constructor(message = 'Bad request') {
|
|
25
|
+
super(message, 400, 'BAD_REQUEST');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class UnauthorizedError extends AppError {
|
|
30
|
+
constructor(message = 'Unauthorized') {
|
|
31
|
+
super(message, 401, 'UNAUTHORIZED');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ForbiddenError extends AppError {
|
|
36
|
+
constructor(message = 'Forbidden') {
|
|
37
|
+
super(message, 403, 'FORBIDDEN');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class ConflictError extends AppError {
|
|
42
|
+
constructor(message = 'Conflict') {
|
|
43
|
+
super(message, 409, 'CONFLICT');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class InternalServerError extends AppError {
|
|
48
|
+
constructor(message = 'Internal server error') {
|
|
49
|
+
super(message, 500, 'INTERNAL_SERVER_ERROR');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Error handler middleware
|
|
55
|
+
* @param {Object} options - Error handler options
|
|
56
|
+
* @returns {Function} - Error handling middleware
|
|
57
|
+
*/
|
|
58
|
+
function errorHandler(options = {}) {
|
|
59
|
+
const {
|
|
60
|
+
format = 'json',
|
|
61
|
+
includeStack = process.env.NODE_ENV === 'development',
|
|
62
|
+
logErrors = true,
|
|
63
|
+
logger = console.error,
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
return async (err, req, res, next) => {
|
|
67
|
+
// Determine status code
|
|
68
|
+
const statusCode = err.statusCode || err.status || 500;
|
|
69
|
+
const code = err.code || 'INTERNAL_SERVER_ERROR';
|
|
70
|
+
const message = err.message || 'Internal server error';
|
|
71
|
+
|
|
72
|
+
// Log error
|
|
73
|
+
if (logErrors) {
|
|
74
|
+
logger('Error:', {
|
|
75
|
+
message,
|
|
76
|
+
statusCode,
|
|
77
|
+
code,
|
|
78
|
+
stack: includeStack ? err.stack : undefined,
|
|
79
|
+
path: req.path,
|
|
80
|
+
method: req.method,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Format error response
|
|
85
|
+
const errorResponse = {
|
|
86
|
+
error: {
|
|
87
|
+
message,
|
|
88
|
+
code,
|
|
89
|
+
statusCode,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Include stack trace in development
|
|
94
|
+
if (includeStack && err.stack) {
|
|
95
|
+
errorResponse.error.stack = err.stack.split('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Include validation errors if present
|
|
99
|
+
if (err.errors && Array.isArray(err.errors)) {
|
|
100
|
+
errorResponse.error.errors = err.errors;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Set response
|
|
104
|
+
res.statusCode = statusCode;
|
|
105
|
+
|
|
106
|
+
if (format === 'json') {
|
|
107
|
+
res.headers = res.headers || {};
|
|
108
|
+
res.headers['Content-Type'] = 'application/json';
|
|
109
|
+
res.body = errorResponse;
|
|
110
|
+
} else {
|
|
111
|
+
res.body = `${statusCode} ${message}`;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Async error wrapper
|
|
118
|
+
* Wraps async route handlers to catch errors
|
|
119
|
+
* @param {Function} fn - Async function
|
|
120
|
+
* @returns {Function} - Wrapped function
|
|
121
|
+
*/
|
|
122
|
+
function asyncHandler(fn) {
|
|
123
|
+
return (req, res, next) => {
|
|
124
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Not found handler
|
|
130
|
+
* @returns {Function} - Middleware function
|
|
131
|
+
*/
|
|
132
|
+
function notFoundHandler() {
|
|
133
|
+
return (req, res) => {
|
|
134
|
+
res.statusCode = 404;
|
|
135
|
+
res.body = {
|
|
136
|
+
error: {
|
|
137
|
+
message: `Route ${req.method} ${req.path} not found`,
|
|
138
|
+
code: 'NOT_FOUND',
|
|
139
|
+
statusCode: 404,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
AppError,
|
|
147
|
+
NotFoundError,
|
|
148
|
+
BadRequestError,
|
|
149
|
+
UnauthorizedError,
|
|
150
|
+
ForbiddenError,
|
|
151
|
+
ConflictError,
|
|
152
|
+
InternalServerError,
|
|
153
|
+
errorHandler,
|
|
154
|
+
asyncHandler,
|
|
155
|
+
notFoundHandler,
|
|
156
|
+
};
|
|
157
|
+
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,37 @@ const Logger = require('./observability/logger');
|
|
|
16
16
|
const Metrics = require('./observability/metrics');
|
|
17
17
|
const Tracer = require('./observability/tracer');
|
|
18
18
|
|
|
19
|
+
// v3.1: Lambda Optimizations
|
|
20
|
+
const { getPool, ServiceClientPool } = require('./utils/service-client-pool');
|
|
21
|
+
const { LazyInit, createLazyInit } = require('./utils/lazy-init');
|
|
22
|
+
const LambdaHandler = require('./core/lambda-handler');
|
|
23
|
+
const { coldStartTracker } = require('./middleware/cold-start-tracker');
|
|
24
|
+
|
|
25
|
+
// v4: Advanced Features
|
|
26
|
+
const AdvancedRouter = require('./core/advanced-router');
|
|
27
|
+
const { validate, ValidationError } = require('./validation/validator');
|
|
28
|
+
const {
|
|
29
|
+
authenticateJWT,
|
|
30
|
+
authenticateAPIKey,
|
|
31
|
+
authorize,
|
|
32
|
+
optionalAuth,
|
|
33
|
+
AuthenticationError,
|
|
34
|
+
AuthorizationError,
|
|
35
|
+
} = require('./auth/authenticator');
|
|
36
|
+
const { rateLimit, RateLimiter } = require('./middleware/rate-limiter');
|
|
37
|
+
const {
|
|
38
|
+
AppError,
|
|
39
|
+
NotFoundError,
|
|
40
|
+
BadRequestError,
|
|
41
|
+
UnauthorizedError,
|
|
42
|
+
ForbiddenError,
|
|
43
|
+
ConflictError,
|
|
44
|
+
InternalServerError,
|
|
45
|
+
errorHandler,
|
|
46
|
+
asyncHandler,
|
|
47
|
+
notFoundHandler,
|
|
48
|
+
} = require('./errors/error-handler');
|
|
49
|
+
|
|
19
50
|
module.exports = {
|
|
20
51
|
// Core
|
|
21
52
|
NavisApp,
|
|
@@ -38,6 +69,37 @@ module.exports = {
|
|
|
38
69
|
Metrics,
|
|
39
70
|
Tracer,
|
|
40
71
|
|
|
72
|
+
// v3.1: Lambda Optimizations
|
|
73
|
+
ServiceClientPool,
|
|
74
|
+
getPool,
|
|
75
|
+
LazyInit,
|
|
76
|
+
createLazyInit,
|
|
77
|
+
LambdaHandler,
|
|
78
|
+
coldStartTracker,
|
|
79
|
+
|
|
80
|
+
// v4: Advanced Features
|
|
81
|
+
AdvancedRouter,
|
|
82
|
+
validate,
|
|
83
|
+
ValidationError,
|
|
84
|
+
authenticateJWT,
|
|
85
|
+
authenticateAPIKey,
|
|
86
|
+
authorize,
|
|
87
|
+
optionalAuth,
|
|
88
|
+
AuthenticationError,
|
|
89
|
+
AuthorizationError,
|
|
90
|
+
rateLimit,
|
|
91
|
+
RateLimiter,
|
|
92
|
+
AppError,
|
|
93
|
+
NotFoundError,
|
|
94
|
+
BadRequestError,
|
|
95
|
+
UnauthorizedError,
|
|
96
|
+
ForbiddenError,
|
|
97
|
+
ConflictError,
|
|
98
|
+
InternalServerError,
|
|
99
|
+
errorHandler,
|
|
100
|
+
asyncHandler,
|
|
101
|
+
notFoundHandler,
|
|
102
|
+
|
|
41
103
|
// Utilities
|
|
42
104
|
response: {
|
|
43
105
|
success,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cold Start Tracker Middleware
|
|
3
|
+
* v3.1: Track and log cold start metrics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let isFirstInvocation = true;
|
|
7
|
+
let initTime = Date.now();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Cold start tracking middleware
|
|
11
|
+
* Tracks cold starts and adds headers to response
|
|
12
|
+
* @param {Object} req - Request object
|
|
13
|
+
* @param {Object} res - Response object
|
|
14
|
+
* @param {Function} next - Next middleware
|
|
15
|
+
*/
|
|
16
|
+
function coldStartTracker(req, res, next) {
|
|
17
|
+
if (isFirstInvocation) {
|
|
18
|
+
const coldStartDuration = Date.now() - initTime;
|
|
19
|
+
|
|
20
|
+
// Add cold start info to response headers
|
|
21
|
+
if (res.headers) {
|
|
22
|
+
res.headers['X-Cold-Start'] = 'true';
|
|
23
|
+
res.headers['X-Cold-Start-Duration'] = coldStartDuration.toString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Log cold start (structured logging)
|
|
27
|
+
console.log(JSON.stringify({
|
|
28
|
+
type: 'cold-start',
|
|
29
|
+
duration: coldStartDuration,
|
|
30
|
+
path: req.path || req.url,
|
|
31
|
+
method: req.method,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
isFirstInvocation = false;
|
|
35
|
+
} else {
|
|
36
|
+
if (res.headers) {
|
|
37
|
+
res.headers['X-Cold-Start'] = 'false';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
next();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reset cold start tracker (useful for testing)
|
|
46
|
+
*/
|
|
47
|
+
function resetColdStartTracker() {
|
|
48
|
+
isFirstInvocation = true;
|
|
49
|
+
initTime = Date.now();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
coldStartTracker,
|
|
54
|
+
resetColdStartTracker,
|
|
55
|
+
};
|
|
56
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiting Middleware
|
|
3
|
+
* v4: In-memory rate limiting with configurable windows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class RateLimiter {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.windowMs = options.windowMs || 60000; // 1 minute default
|
|
9
|
+
this.max = options.max || 100; // 100 requests default
|
|
10
|
+
this.store = new Map(); // In-memory store
|
|
11
|
+
this.skipSuccessfulRequests = options.skipSuccessfulRequests || false;
|
|
12
|
+
this.skipFailedRequests = options.skipFailedRequests || false;
|
|
13
|
+
this.keyGenerator = options.keyGenerator || ((req) => {
|
|
14
|
+
// Default: IP address
|
|
15
|
+
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
|
16
|
+
req.headers['x-real-ip'] ||
|
|
17
|
+
'unknown';
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Cleanup old entries periodically
|
|
21
|
+
this.cleanupInterval = setInterval(() => {
|
|
22
|
+
this._cleanup();
|
|
23
|
+
}, this.windowMs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Rate limit middleware
|
|
28
|
+
* @returns {Function} - Middleware function
|
|
29
|
+
*/
|
|
30
|
+
middleware() {
|
|
31
|
+
const self = this; // Capture 'this' reference
|
|
32
|
+
|
|
33
|
+
return async (req, res, next) => {
|
|
34
|
+
const key = self.keyGenerator(req);
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
|
|
37
|
+
// Get or create rate limit entry
|
|
38
|
+
let entry = self.store.get(key);
|
|
39
|
+
|
|
40
|
+
if (!entry || now - entry.resetTime > self.windowMs) {
|
|
41
|
+
// Create new entry or reset expired one
|
|
42
|
+
entry = {
|
|
43
|
+
count: 0,
|
|
44
|
+
resetTime: now + self.windowMs,
|
|
45
|
+
};
|
|
46
|
+
self.store.set(key, entry);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Increment count
|
|
50
|
+
entry.count++;
|
|
51
|
+
|
|
52
|
+
// Set rate limit headers
|
|
53
|
+
res.headers = res.headers || {};
|
|
54
|
+
res.headers['X-RateLimit-Limit'] = self.max.toString();
|
|
55
|
+
res.headers['X-RateLimit-Remaining'] = Math.max(0, self.max - entry.count).toString();
|
|
56
|
+
res.headers['X-RateLimit-Reset'] = new Date(entry.resetTime).toISOString();
|
|
57
|
+
|
|
58
|
+
// Check if limit exceeded
|
|
59
|
+
if (entry.count > self.max) {
|
|
60
|
+
res.statusCode = 429;
|
|
61
|
+
res.body = {
|
|
62
|
+
error: 'Too many requests',
|
|
63
|
+
retryAfter: Math.ceil((entry.resetTime - now) / 1000),
|
|
64
|
+
};
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Track response status for skip options
|
|
69
|
+
const originalStatusCode = res.statusCode;
|
|
70
|
+
const originalFinish = res.finish || (() => {});
|
|
71
|
+
|
|
72
|
+
// Wrap response finish to track status
|
|
73
|
+
res.finish = function(...args) {
|
|
74
|
+
const statusCode = res.statusCode || 200;
|
|
75
|
+
|
|
76
|
+
if (self.skipSuccessfulRequests && statusCode < 400) {
|
|
77
|
+
entry.count = Math.max(0, entry.count - 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (self.skipFailedRequests && statusCode >= 400) {
|
|
81
|
+
entry.count = Math.max(0, entry.count - 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return originalFinish.apply(this, args);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
next();
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Cleanup expired entries
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_cleanup() {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
for (const [key, entry] of this.store.entries()) {
|
|
98
|
+
if (now - entry.resetTime > this.windowMs) {
|
|
99
|
+
this.store.delete(key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reset rate limit for a key
|
|
106
|
+
* @param {string} key - Rate limit key
|
|
107
|
+
*/
|
|
108
|
+
reset(key) {
|
|
109
|
+
this.store.delete(key);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get rate limit info for a key
|
|
114
|
+
* @param {string} key - Rate limit key
|
|
115
|
+
* @returns {Object|null} - Rate limit info
|
|
116
|
+
*/
|
|
117
|
+
get(key) {
|
|
118
|
+
const entry = this.store.get(key);
|
|
119
|
+
if (!entry) return null;
|
|
120
|
+
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
if (now - entry.resetTime > this.windowMs) {
|
|
123
|
+
this.store.delete(key);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
count: entry.count,
|
|
129
|
+
remaining: Math.max(0, this.max - entry.count),
|
|
130
|
+
resetTime: entry.resetTime,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Destroy rate limiter (cleanup)
|
|
136
|
+
*/
|
|
137
|
+
destroy() {
|
|
138
|
+
if (this.cleanupInterval) {
|
|
139
|
+
clearInterval(this.cleanupInterval);
|
|
140
|
+
}
|
|
141
|
+
this.store.clear();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create rate limit middleware
|
|
147
|
+
* @param {Object} options - Rate limit options
|
|
148
|
+
* @returns {Function} - Middleware function
|
|
149
|
+
*/
|
|
150
|
+
function rateLimit(options = {}) {
|
|
151
|
+
const limiter = new RateLimiter(options);
|
|
152
|
+
return limiter.middleware();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
rateLimit,
|
|
157
|
+
RateLimiter,
|
|
158
|
+
};
|
|
159
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy Initialization Utility
|
|
3
|
+
* v3.1: Defer heavy initialization until needed (reduces cold start time)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class LazyInit {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.initialized = false;
|
|
9
|
+
this.initPromise = null;
|
|
10
|
+
this.initFn = null;
|
|
11
|
+
this.autoInit = options.autoInit !== false; // Auto-init on first access
|
|
12
|
+
this.cacheResult = options.cacheResult !== false; // Cache initialization result
|
|
13
|
+
this.cachedResult = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize with a function
|
|
18
|
+
* @param {Function} initFn - Initialization function (can be async)
|
|
19
|
+
* @returns {Promise} - Initialization promise
|
|
20
|
+
*/
|
|
21
|
+
async init(initFn) {
|
|
22
|
+
if (this.initialized && this.cacheResult) {
|
|
23
|
+
return this.cachedResult;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!this.initPromise) {
|
|
27
|
+
this.initFn = initFn;
|
|
28
|
+
this.initPromise = Promise.resolve(initFn()).then(result => {
|
|
29
|
+
this.initialized = true;
|
|
30
|
+
if (this.cacheResult) {
|
|
31
|
+
this.cachedResult = result;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}).catch(error => {
|
|
35
|
+
// Reset on error so it can be retried
|
|
36
|
+
this.initPromise = null;
|
|
37
|
+
throw error;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this.initPromise;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if initialized
|
|
46
|
+
* @returns {boolean} - True if initialized
|
|
47
|
+
*/
|
|
48
|
+
isInitialized() {
|
|
49
|
+
return this.initialized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get cached result (if available)
|
|
54
|
+
* @returns {*} - Cached initialization result
|
|
55
|
+
*/
|
|
56
|
+
getCached() {
|
|
57
|
+
return this.cachedResult;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reset initialization state
|
|
62
|
+
*/
|
|
63
|
+
reset() {
|
|
64
|
+
this.initialized = false;
|
|
65
|
+
this.initPromise = null;
|
|
66
|
+
this.cachedResult = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Execute function with lazy initialization
|
|
71
|
+
* @param {Function} fn - Function to execute after initialization
|
|
72
|
+
* @returns {Promise} - Result of function execution
|
|
73
|
+
*/
|
|
74
|
+
async withInit(fn) {
|
|
75
|
+
if (!this.initialized && this.initFn) {
|
|
76
|
+
await this.init(this.initFn);
|
|
77
|
+
}
|
|
78
|
+
return await fn(this.cachedResult);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a lazy initializer
|
|
84
|
+
* @param {Function} initFn - Initialization function
|
|
85
|
+
* @param {Object} options - Options
|
|
86
|
+
* @returns {LazyInit} - LazyInit instance
|
|
87
|
+
*/
|
|
88
|
+
function createLazyInit(initFn, options = {}) {
|
|
89
|
+
const lazy = new LazyInit(options);
|
|
90
|
+
if (initFn) {
|
|
91
|
+
lazy.initFn = initFn;
|
|
92
|
+
}
|
|
93
|
+
return lazy;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
LazyInit,
|
|
98
|
+
createLazyInit,
|
|
99
|
+
};
|
|
100
|
+
|