mastercontroller 1.2.13 → 1.3.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/.claude/settings.local.json +12 -0
- package/MasterAction.js +7 -7
- package/MasterControl.js +192 -122
- package/MasterCors.js +29 -0
- package/MasterHtml.js +5 -5
- package/MasterPipeline.js +344 -0
- package/MasterRouter.js +59 -29
- package/MasterSession.js +19 -0
- package/MasterTemplate.js +3 -3
- package/MasterTimeout.js +332 -0
- package/README.md +1496 -36
- package/docs/timeout-and-error-handling.md +712 -0
- package/{MasterError.js → error/MasterError.js} +2 -2
- package/{MasterErrorLogger.js → error/MasterErrorLogger.js} +1 -1
- package/{MasterErrorMiddleware.js → error/MasterErrorMiddleware.js} +2 -2
- package/error/MasterErrorRenderer.js +529 -0
- package/{ssr → error}/SSRErrorHandler.js +2 -2
- package/{MasterCache.js → monitoring/MasterCache.js} +2 -2
- package/{MasterMemoryMonitor.js → monitoring/MasterMemoryMonitor.js} +2 -2
- package/{MasterProfiler.js → monitoring/MasterProfiler.js} +2 -2
- package/{ssr → monitoring}/PerformanceMonitor.js +2 -2
- package/package.json +5 -5
- package/{EventHandlerValidator.js → security/EventHandlerValidator.js} +3 -3
- package/{MasterSanitizer.js → security/MasterSanitizer.js} +2 -2
- package/{MasterValidator.js → security/MasterValidator.js} +2 -2
- package/{SecurityMiddleware.js → security/SecurityMiddleware.js} +75 -3
- package/{SessionSecurity.js → security/SessionSecurity.js} +2 -2
- package/ssr/hydration-client.js +3 -3
- package/ssr/runtime-ssr.cjs +9 -9
- package/MasterBenchmark.js +0 -89
- package/MasterBuildOptimizer.js +0 -376
- package/MasterBundleAnalyzer.js +0 -108
- package/ssr/HTMLUtils.js +0 -15
- /package/{ssr → error}/ErrorBoundary.js +0 -0
- /package/{ssr → error}/HydrationMismatch.js +0 -0
- /package/{MasterBackendErrorHandler.js → error/MasterBackendErrorHandler.js} +0 -0
- /package/{MasterErrorHandler.js → error/MasterErrorHandler.js} +0 -0
- /package/{CSPConfig.js → security/CSPConfig.js} +0 -0
package/MasterTimeout.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MasterTimeout - Professional timeout system for MasterController
|
|
3
|
+
*
|
|
4
|
+
* Provides per-request timeout tracking with configurable options:
|
|
5
|
+
* - Global timeout (all requests)
|
|
6
|
+
* - Route-specific timeouts
|
|
7
|
+
* - Controller-level timeouts
|
|
8
|
+
* - Graceful cleanup on timeout
|
|
9
|
+
* - Detailed timeout logging
|
|
10
|
+
*
|
|
11
|
+
* Inspired by Rails ActionController::Timeout and Django MIDDLEWARE_TIMEOUT
|
|
12
|
+
*
|
|
13
|
+
* @version 1.0.0
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
var master = require('./MasterControl');
|
|
17
|
+
const { logger } = require('./error/MasterErrorLogger');
|
|
18
|
+
|
|
19
|
+
class MasterTimeout {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.globalTimeout = 120000; // 120 seconds default
|
|
22
|
+
this.routeTimeouts = new Map();
|
|
23
|
+
this.activeRequests = new Map();
|
|
24
|
+
this.timeoutHandlers = [];
|
|
25
|
+
this.enabled = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize timeout system
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} options - Configuration options
|
|
32
|
+
* @param {Number} options.globalTimeout - Default timeout in ms (default: 120000)
|
|
33
|
+
* @param {Boolean} options.enabled - Enable/disable timeouts (default: true)
|
|
34
|
+
* @param {Function} options.onTimeout - Custom timeout handler
|
|
35
|
+
*/
|
|
36
|
+
init(options = {}) {
|
|
37
|
+
if (options.globalTimeout) {
|
|
38
|
+
this.globalTimeout = options.globalTimeout;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.enabled !== undefined) {
|
|
42
|
+
this.enabled = options.enabled;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (options.onTimeout && typeof options.onTimeout === 'function') {
|
|
46
|
+
this.timeoutHandlers.push(options.onTimeout);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.info({
|
|
50
|
+
code: 'MC_TIMEOUT_INIT',
|
|
51
|
+
message: 'Timeout system initialized',
|
|
52
|
+
globalTimeout: this.globalTimeout,
|
|
53
|
+
enabled: this.enabled
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set timeout for specific route pattern
|
|
61
|
+
*
|
|
62
|
+
* @param {String} routePattern - Route pattern (e.g., '/api/*', '/admin/reports')
|
|
63
|
+
* @param {Number} timeout - Timeout in milliseconds
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* master.timeout.setRouteTimeout('/api/*', 30000); // 30 seconds for APIs
|
|
67
|
+
* master.timeout.setRouteTimeout('/admin/reports', 300000); // 5 minutes for reports
|
|
68
|
+
*/
|
|
69
|
+
setRouteTimeout(routePattern, timeout) {
|
|
70
|
+
if (typeof timeout !== 'number' || timeout <= 0) {
|
|
71
|
+
throw new Error('Timeout must be a positive number in milliseconds');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.routeTimeouts.set(routePattern, timeout);
|
|
75
|
+
|
|
76
|
+
logger.info({
|
|
77
|
+
code: 'MC_TIMEOUT_ROUTE_SET',
|
|
78
|
+
message: 'Route timeout configured',
|
|
79
|
+
routePattern,
|
|
80
|
+
timeout
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get timeout for request based on route
|
|
88
|
+
* Priority: Route-specific > Global
|
|
89
|
+
*
|
|
90
|
+
* @param {String} path - Request path
|
|
91
|
+
* @returns {Number} - Timeout in milliseconds
|
|
92
|
+
*/
|
|
93
|
+
getTimeoutForPath(path) {
|
|
94
|
+
// Check route-specific timeouts
|
|
95
|
+
for (const [pattern, timeout] of this.routeTimeouts.entries()) {
|
|
96
|
+
if (this._pathMatches(path, pattern)) {
|
|
97
|
+
return timeout;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Return global timeout
|
|
102
|
+
return this.globalTimeout;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Start timeout tracking for request
|
|
107
|
+
*
|
|
108
|
+
* @param {Object} ctx - Request context
|
|
109
|
+
* @returns {String} - Request ID
|
|
110
|
+
*/
|
|
111
|
+
startTracking(ctx) {
|
|
112
|
+
if (!this.enabled) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const requestId = this._generateRequestId();
|
|
117
|
+
const timeout = this.getTimeoutForPath(ctx.pathName || ctx.request.url);
|
|
118
|
+
const startTime = Date.now();
|
|
119
|
+
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
this._handleTimeout(requestId, ctx, startTime);
|
|
122
|
+
}, timeout);
|
|
123
|
+
|
|
124
|
+
this.activeRequests.set(requestId, {
|
|
125
|
+
timer,
|
|
126
|
+
timeout,
|
|
127
|
+
startTime,
|
|
128
|
+
path: ctx.pathName || ctx.request.url,
|
|
129
|
+
method: ctx.type || ctx.request.method.toLowerCase()
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Attach cleanup to response finish
|
|
133
|
+
ctx.response.once('finish', () => {
|
|
134
|
+
this.stopTracking(requestId);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
ctx.response.once('close', () => {
|
|
138
|
+
this.stopTracking(requestId);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return requestId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Stop timeout tracking for request
|
|
146
|
+
*
|
|
147
|
+
* @param {String} requestId - Request ID
|
|
148
|
+
*/
|
|
149
|
+
stopTracking(requestId) {
|
|
150
|
+
const tracked = this.activeRequests.get(requestId);
|
|
151
|
+
|
|
152
|
+
if (tracked) {
|
|
153
|
+
clearTimeout(tracked.timer);
|
|
154
|
+
this.activeRequests.delete(requestId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle request timeout
|
|
160
|
+
*
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
_handleTimeout(requestId, ctx, startTime) {
|
|
164
|
+
const tracked = this.activeRequests.get(requestId);
|
|
165
|
+
|
|
166
|
+
if (!tracked) {
|
|
167
|
+
return; // Already cleaned up
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const duration = Date.now() - startTime;
|
|
171
|
+
|
|
172
|
+
// Log timeout
|
|
173
|
+
logger.error({
|
|
174
|
+
code: 'MC_REQUEST_TIMEOUT',
|
|
175
|
+
message: 'Request timeout exceeded',
|
|
176
|
+
requestId,
|
|
177
|
+
path: tracked.path,
|
|
178
|
+
method: tracked.method,
|
|
179
|
+
timeout: tracked.timeout,
|
|
180
|
+
duration
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Call custom handlers
|
|
184
|
+
for (const handler of this.timeoutHandlers) {
|
|
185
|
+
try {
|
|
186
|
+
handler(ctx, {
|
|
187
|
+
requestId,
|
|
188
|
+
path: tracked.path,
|
|
189
|
+
method: tracked.method,
|
|
190
|
+
timeout: tracked.timeout,
|
|
191
|
+
duration
|
|
192
|
+
});
|
|
193
|
+
} catch (err) {
|
|
194
|
+
logger.error({
|
|
195
|
+
code: 'MC_TIMEOUT_HANDLER_ERROR',
|
|
196
|
+
message: 'Timeout handler threw error',
|
|
197
|
+
error: err.message
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Send timeout response if not already sent
|
|
203
|
+
if (!ctx.response.headersSent) {
|
|
204
|
+
ctx.response.statusCode = 504; // Gateway Timeout
|
|
205
|
+
ctx.response.setHeader('Content-Type', 'application/json');
|
|
206
|
+
ctx.response.end(JSON.stringify({
|
|
207
|
+
error: 'Request Timeout',
|
|
208
|
+
message: 'The server did not receive a complete request within the allowed time',
|
|
209
|
+
code: 'MC_REQUEST_TIMEOUT',
|
|
210
|
+
timeout: tracked.timeout
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Cleanup
|
|
215
|
+
this.stopTracking(requestId);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get middleware function for pipeline
|
|
220
|
+
*
|
|
221
|
+
* @returns {Function} - Middleware function
|
|
222
|
+
*/
|
|
223
|
+
middleware() {
|
|
224
|
+
const $that = this;
|
|
225
|
+
|
|
226
|
+
return async (ctx, next) => {
|
|
227
|
+
if (!$that.enabled) {
|
|
228
|
+
await next();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const requestId = $that.startTracking(ctx);
|
|
233
|
+
ctx.requestId = requestId;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await next();
|
|
237
|
+
} catch (err) {
|
|
238
|
+
// Stop tracking on error
|
|
239
|
+
$that.stopTracking(requestId);
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Disable timeouts (useful for debugging)
|
|
247
|
+
*/
|
|
248
|
+
disable() {
|
|
249
|
+
this.enabled = false;
|
|
250
|
+
logger.info({
|
|
251
|
+
code: 'MC_TIMEOUT_DISABLED',
|
|
252
|
+
message: 'Timeout system disabled'
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Enable timeouts
|
|
258
|
+
*/
|
|
259
|
+
enable() {
|
|
260
|
+
this.enabled = true;
|
|
261
|
+
logger.info({
|
|
262
|
+
code: 'MC_TIMEOUT_ENABLED',
|
|
263
|
+
message: 'Timeout system enabled'
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get current timeout statistics
|
|
269
|
+
*
|
|
270
|
+
* @returns {Object} - Timeout stats
|
|
271
|
+
*/
|
|
272
|
+
getStats() {
|
|
273
|
+
return {
|
|
274
|
+
enabled: this.enabled,
|
|
275
|
+
globalTimeout: this.globalTimeout,
|
|
276
|
+
routeTimeouts: Array.from(this.routeTimeouts.entries()).map(([pattern, timeout]) => ({
|
|
277
|
+
pattern,
|
|
278
|
+
timeout
|
|
279
|
+
})),
|
|
280
|
+
activeRequests: this.activeRequests.size,
|
|
281
|
+
requests: Array.from(this.activeRequests.entries()).map(([id, data]) => ({
|
|
282
|
+
requestId: id,
|
|
283
|
+
path: data.path,
|
|
284
|
+
method: data.method,
|
|
285
|
+
timeout: data.timeout,
|
|
286
|
+
elapsed: Date.now() - data.startTime,
|
|
287
|
+
remaining: data.timeout - (Date.now() - data.startTime)
|
|
288
|
+
}))
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if path matches pattern
|
|
294
|
+
*
|
|
295
|
+
* @private
|
|
296
|
+
*/
|
|
297
|
+
_pathMatches(path, pattern) {
|
|
298
|
+
if (typeof pattern === 'string') {
|
|
299
|
+
// Normalize paths
|
|
300
|
+
const normalizedPath = '/' + path.replace(/^\/|\/$/g, '');
|
|
301
|
+
const normalizedPattern = '/' + pattern.replace(/^\/|\/$/g, '');
|
|
302
|
+
|
|
303
|
+
// Wildcard support
|
|
304
|
+
if (normalizedPattern.endsWith('/*')) {
|
|
305
|
+
const prefix = normalizedPattern.slice(0, -2);
|
|
306
|
+
return normalizedPath === prefix || normalizedPath.startsWith(prefix + '/');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Exact match
|
|
310
|
+
return normalizedPath === normalizedPattern;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (pattern instanceof RegExp) {
|
|
314
|
+
return pattern.test(path);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Generate unique request ID
|
|
322
|
+
*
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
_generateRequestId() {
|
|
326
|
+
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
master.extend("timeout", MasterTimeout);
|
|
331
|
+
|
|
332
|
+
module.exports = MasterTimeout;
|