mastercontroller 1.2.14 → 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/MasterRouter.js CHANGED
@@ -246,88 +246,110 @@ var loadScopedListClasses = function(){
246
246
  };
247
247
 
248
248
 
249
+ /**
250
+ * Normalize route path: lowercase segments but preserve param names
251
+ *
252
+ * @param {String} path - Route path like "/Period/:periodId/Items/:itemId"
253
+ * @returns {String} - Normalized: "period/:periodId/items/:itemId"
254
+ */
255
+ function normalizeRoutePath(path) {
256
+ const trimmed = path.replace(/^\/|\/$/g, '');
257
+ const segments = trimmed.split('/');
258
+
259
+ const normalized = segments.map(segment => {
260
+ // Preserve parameter names (start with :)
261
+ if (segment.startsWith(':')) {
262
+ return segment;
263
+ }
264
+ // Lowercase path segments
265
+ return segment.toLowerCase();
266
+ });
267
+
268
+ return normalized.join('/');
269
+ }
270
+
249
271
  class MasterRouter {
250
272
  currentRouteName = null
251
273
  _routes = {}
252
-
274
+
253
275
  start(){
254
276
  var $that = this;
255
277
  return {
256
278
  route : function(path, toPath, type, constraint){ // function to add to list of routes
257
-
279
+
258
280
  var pathList = toPath.replace(/^\/|\/$/g, '').split("#");
259
-
281
+
260
282
  var route = {
261
283
  type: type.toLowerCase(),
262
- path: path.replace(/^\/|\/$/g, '').toLowerCase(),
284
+ path: normalizeRoutePath(path),
263
285
  toController :pathList[0].replace(/^\/|\/$/g, ''),
264
286
  toAction: pathList[1],
265
287
  constraint : constraint
266
288
  };
267
-
289
+
268
290
  $that._routes[$that.currentRouteName].routes.push(route);
269
-
291
+
270
292
  },
271
293
 
272
294
  resources: function(routeName){ // function to add to list of routes using resources bulk
273
-
295
+
274
296
 
275
297
  $that._routes[$that.currentRouteName].routes.push({
276
298
  type: "get",
277
- path: routeName.toLowerCase(),
299
+ path: normalizeRoutePath(routeName),
278
300
  toController :routeName,
279
301
  toAction: "index",
280
302
  constraint : null
281
303
  });
282
-
304
+
283
305
  $that._routes[$that.currentRouteName].routes.push({
284
306
  type: "get",
285
- path: routeName.toLowerCase(),
307
+ path: normalizeRoutePath(routeName),
286
308
  toController :routeName,
287
309
  toAction: "new",
288
310
  constraint : null
289
311
  });
290
-
312
+
291
313
  $that._routes[$that.currentRouteName].routes.push({
292
314
  type: "post",
293
- path: routeName.toLowerCase(),
315
+ path: normalizeRoutePath(routeName),
294
316
  toController :routeName,
295
317
  toAction: "create",
296
318
  constraint : null
297
319
  });
298
-
320
+
299
321
  $that._routes[$that.currentRouteName].routes.push({
300
322
  // pages/3
301
323
  type: "get",
302
- path: routeName.toLowerCase() + "/:id",
324
+ path: normalizeRoutePath(routeName + "/:id"),
303
325
  toController :routeName,
304
326
  toAction: "show",
305
327
  constraint : null
306
328
  });
307
-
329
+
308
330
  $that._routes[$that.currentRouteName].routes.push({
309
331
  type: "get",
310
- path: routeName.toLowerCase() + "/:id/" + "edit",
332
+ path: normalizeRoutePath(routeName + "/:id/edit"),
311
333
  toController :routeName,
312
334
  toAction: "edit",
313
- constraint : null
335
+ constraint : null
314
336
  });
315
-
337
+
316
338
  $that._routes[$that.currentRouteName].routes.push({
317
339
  type: "put",
318
- path: routeName.toLowerCase() + "/:id",
340
+ path: normalizeRoutePath(routeName + "/:id"),
319
341
  toController :routeName,
320
342
  toAction: "update",
321
343
  constraint : null
322
344
  });
323
-
345
+
324
346
  $that._routes[$that.currentRouteName].routes.push({
325
347
  type: "delete",
326
- path: routeName.toLowerCase() + "/:id",
348
+ path: normalizeRoutePath(routeName + "/:id"),
327
349
  toController :routeName,
328
350
  toAction: "destroy",
329
351
  constraint : null
330
- });
352
+ });
331
353
  }
332
354
  }
333
355
  }
package/MasterSession.js CHANGED
@@ -35,6 +35,11 @@ class MasterSession{
35
35
  this.options.secret = TID;
36
36
  }
37
37
 
38
+ // Auto-register with pipeline if available
39
+ if (master.pipeline) {
40
+ master.pipeline.use(this.middleware());
41
+ }
42
+
38
43
  return {
39
44
  setPath : function(path){
40
45
  $that.options.path = path === undefined ? '/' : path;
@@ -184,6 +189,20 @@ class MasterSession{
184
189
  return -1;
185
190
  }
186
191
  }
192
+
193
+ /**
194
+ * Get session middleware for the pipeline
195
+ * Sessions are accessed lazily via master.sessions in controllers
196
+ */
197
+ middleware() {
198
+ var $that = this;
199
+
200
+ return async (ctx, next) => {
201
+ // Sessions are available via master.sessions.get/set in controllers
202
+ // No action needed here - just continue pipeline
203
+ await next();
204
+ };
205
+ }
187
206
  }
188
207
 
189
208
  master.extend("sessions", MasterSession);
@@ -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;