mastercontroller 1.3.14 → 1.3.16

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
@@ -1,10 +1,10 @@
1
1
  // version 0.0.250
2
2
 
3
- var toolClass = require('./MasterTools');
3
+ const toolClass = require('./MasterTools');
4
4
  const EventEmitter = require("events");
5
- var path = require('path');
6
- var currentRoute = {};
7
- var tools = new toolClass();
5
+ const path = require('path');
6
+ // REMOVED: Global currentRoute (race condition bug) - now stored in requestObject
7
+ const tools = new toolClass();
8
8
 
9
9
  // Enhanced error handling
10
10
  const { handleRoutingError, handleControllerError, sendErrorResponse } = require('./error/MasterBackendErrorHandler');
@@ -17,18 +17,69 @@ const { escapeHTML } = require('./security/MasterSanitizer');
17
17
 
18
18
  const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.master === 'development';
19
19
 
20
- var normalizePaths = function(requestPath, routePath, requestParams){
21
- var obj = {
20
+ // HTTP Status Code Constants
21
+ const HTTP_STATUS = {
22
+ OK: 200,
23
+ NO_CONTENT: 204,
24
+ BAD_REQUEST: 400,
25
+ NOT_FOUND: 404,
26
+ INTERNAL_ERROR: 500
27
+ };
28
+
29
+ // Event Names Constants
30
+ const EVENT_NAMES = {
31
+ ROUTE_CONSTRAINT_GOOD: 'routeConstraintGood',
32
+ CONTROLLER: 'controller'
33
+ };
34
+
35
+ // HTTP Methods Constants
36
+ const HTTP_METHODS = {
37
+ GET: 'get',
38
+ POST: 'post',
39
+ PUT: 'put',
40
+ DELETE: 'delete',
41
+ OPTIONS: 'options'
42
+ };
43
+
44
+ // Router Configuration Constants
45
+ const ROUTER_CONFIG = {
46
+ ROUTE_ID_LENGTH: 4,
47
+ DEFAULT_TIMEOUT: 30000, // 30 seconds
48
+ MAX_ROUTE_LENGTH: 2048
49
+ };
50
+
51
+ /**
52
+ * Normalize and match request path against route path, extracting parameters
53
+ *
54
+ * Compares request path segments with route path segments. When a route segment
55
+ * starts with ":", treats it as a parameter and extracts the corresponding value
56
+ * from the request path.
57
+ *
58
+ * @param {string} requestPath - The incoming request path (e.g., "/users/123")
59
+ * @param {string} routePath - The route pattern (e.g., "/users/:id")
60
+ * @param {Object} requestParams - Object to populate with extracted parameters
61
+ * @returns {Object} Object with normalized requestPath and routePath
62
+ * @returns {string} result.requestPath - Original request path
63
+ * @returns {string} result.routePath - Route path with parameters replaced by actual values
64
+ *
65
+ * @example
66
+ * const params = {};
67
+ * const result = normalizePaths("/users/123", "/users/:id", params);
68
+ * // params = { id: "123" }
69
+ * // result = { requestPath: "/users/123", routePath: "/users/123" }
70
+ */
71
+ const normalizePaths = function(requestPath, routePath, requestParams){
72
+ const obj = {
22
73
  requestPath : "",
23
74
  routePath : ""
24
75
  }
25
76
 
26
- var requestPathList = requestPath.split("/");
27
- var routePathList = routePath.split("/");
77
+ const requestPathList = requestPath.split("/");
78
+ const routePathList = routePath.split("/");
28
79
 
29
- for(i = 0; i < requestPathList.length; i++){
30
- requestItem = requestPathList[i];
31
- routeItem = routePathList[i];
80
+ for(let i = 0; i < requestPathList.length; i++){
81
+ const requestItem = requestPathList[i];
82
+ const routeItem = routePathList[i];
32
83
  if(routeItem){
33
84
  if(routeItem.indexOf(":") > -1){
34
85
  const paramName = routeItem.replace(":", "");
@@ -50,8 +101,23 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
50
101
 
51
102
  /**
52
103
  * Sanitize route parameter to prevent injection attacks
104
+ *
105
+ * Checks for and mitigates:
106
+ * - Path traversal attempts (../, ./)
107
+ * - SQL injection patterns
108
+ * - Command injection characters (; | & ` $ ( ))
109
+ *
110
+ * Logs security warnings when attacks are detected.
111
+ *
112
+ * @param {string} paramName - Name of the route parameter
113
+ * @param {string} paramValue - Value to sanitize
114
+ * @returns {string} Sanitized parameter value
115
+ *
116
+ * @example
117
+ * sanitizeRouteParam("id", "123") // Returns: "123"
118
+ * sanitizeRouteParam("path", "../etc/passwd") // Returns: "etcpasswd" (dangerous parts removed)
53
119
  */
54
- var sanitizeRouteParam = function(paramName, paramValue) {
120
+ const sanitizeRouteParam = function(paramName, paramValue) {
55
121
  if (!paramValue || typeof paramValue !== 'string') {
56
122
  return paramValue;
57
123
  }
@@ -102,11 +168,38 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
102
168
  return escapeHTML(paramValue);
103
169
  }
104
170
 
105
- var processRoutes = function(requestObject, emitter, routeObject){
106
- var routeList = routeObject.routes;
107
- var root = routeObject.root;
108
- var isComponent = routeObject.isComponent;
109
- var currentRouteBeingProcessed = null; // Track current route for better error messages
171
+ /**
172
+ * Process routes and match against request
173
+ *
174
+ * Iterates through registered routes, attempting to match the request path and HTTP method.
175
+ * When a match is found:
176
+ * 1. Extracts route parameters
177
+ * 2. Executes route constraints (if defined)
178
+ * 3. Emits EVENT_NAMES.ROUTE_CONSTRAINT_GOOD to trigger controller execution
179
+ *
180
+ * Handles OPTIONS requests for CORS preflight.
181
+ *
182
+ * @param {Object} requestObject - Request context with path, method, params
183
+ * @param {EventEmitter} emitter - Event emitter for route match notification
184
+ * @param {Object} routeObject - Route configuration
185
+ * @param {Array} routeObject.routes - Array of route definitions
186
+ * @param {string} routeObject.root - Application root path
187
+ * @param {boolean} routeObject.isComponent - Whether this is a component route
188
+ * @returns {boolean|number} true if route matched, -1 if no match
189
+ * @throws {Error} If route processing fails
190
+ *
191
+ * @example
192
+ * const result = processRoutes(requestObj, emitter, {
193
+ * routes: [{ path: "/users/:id", type: "get", toController: "users", toAction: "show" }],
194
+ * root: "/app",
195
+ * isComponent: false
196
+ * });
197
+ */
198
+ const processRoutes = function(requestObject, emitter, routeObject){
199
+ const routeList = routeObject.routes;
200
+ const root = routeObject.root;
201
+ const isComponent = routeObject.isComponent;
202
+ let currentRouteBeingProcessed = null; // Track current route for better error messages
110
203
 
111
204
  try{
112
205
  // Ensure routes is an array
@@ -138,8 +231,8 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
138
231
 
139
232
  // FIX: Create a clean copy of params for each route test to prevent parameter pollution
140
233
  // This prevents parameters from non-matching routes from accumulating in requestObject.params
141
- var testParams = Object.assign({}, requestObject.params);
142
- var pathObj = normalizePaths(requestObject.pathName, route.path, testParams);
234
+ const testParams = Object.assign({}, requestObject.params);
235
+ const pathObj = normalizePaths(requestObject.pathName, route.path, testParams);
143
236
 
144
237
  // if we find the route that matches the request
145
238
  if(pathObj.requestPath === pathObj.routePath && route.type === requestObject.type){
@@ -149,17 +242,20 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
149
242
  // call Constraint
150
243
  if(typeof route.constraint === "function"){
151
244
 
152
- var newObj = {};
245
+ const newObj = {};
153
246
  //tools.combineObjects(newObj, this._master.controllerList);
154
247
  newObj.next = function(){
155
- currentRoute.root = root;
156
- currentRoute.pathName = requestObject.pathName;
157
- currentRoute.toAction = requestObject.toAction;
158
- currentRoute.toController = requestObject.toController;
159
- currentRoute.response = requestObject.response;
160
- currentRoute.isComponent = isComponent;
161
- currentRoute.routeDef = currentRouteBeingProcessed; // Add route definition
162
- emitter.emit("routeConstraintGood", requestObject);
248
+ // CRITICAL FIX: Store route info in requestObject instead of global
249
+ requestObject.currentRoute = {
250
+ root,
251
+ pathName: requestObject.pathName,
252
+ toAction: requestObject.toAction,
253
+ toController: requestObject.toController,
254
+ response: requestObject.response,
255
+ isComponent,
256
+ routeDef: currentRouteBeingProcessed
257
+ };
258
+ emitter.emit(EVENT_NAMES.ROUTE_CONSTRAINT_GOOD, requestObject);
163
259
  };
164
260
 
165
261
  // Wrap constraint execution with error handling
@@ -188,24 +284,27 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
188
284
  return true;
189
285
  }else{
190
286
 
191
- currentRoute.root = root;
192
- currentRoute.pathName = requestObject.pathName;
193
- currentRoute.toAction = requestObject.toAction;
194
- currentRoute.toController = requestObject.toController;
195
- currentRoute.response = requestObject.response;
196
- currentRoute.isComponent = isComponent;
197
- currentRoute.routeDef = currentRouteBeingProcessed; // Add route definition
198
- emitter.emit("routeConstraintGood", requestObject);
287
+ // CRITICAL FIX: Store route info in requestObject instead of global
288
+ requestObject.currentRoute = {
289
+ root,
290
+ pathName: requestObject.pathName,
291
+ toAction: requestObject.toAction,
292
+ toController: requestObject.toController,
293
+ response: requestObject.response,
294
+ isComponent,
295
+ routeDef: currentRouteBeingProcessed
296
+ };
297
+ emitter.emit(EVENT_NAMES.ROUTE_CONSTRAINT_GOOD, requestObject);
199
298
  return true;
200
299
  }
201
300
 
202
301
  }
203
302
 
204
- if(pathObj.requestPath === pathObj.routePath && "options" ===requestObject.type.toLowerCase()){
303
+ if(pathObj.requestPath === pathObj.routePath && HTTP_METHODS.OPTIONS === requestObject.type.toLowerCase()){
205
304
  // this means that the request is correct but its an options request means its the browser checking to see if the request is allowed
206
305
  // Commit the params for OPTIONS requests too
207
306
  requestObject.params = testParams;
208
- requestObject.response.writeHead(200, {'Content-Type': 'application/json'});
307
+ requestObject.response.writeHead(HTTP_STATUS.OK, {'Content-Type': 'application/json'});
209
308
  requestObject.response.end(JSON.stringify({"done": "true"}));
210
309
  return true;
211
310
  }
@@ -238,10 +337,23 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
238
337
  }
239
338
  };
240
339
 
241
- // CRITICAL FIX: Race condition - Store scoped services in context instead of shared requestList
242
- // Previously, concurrent requests would overwrite each other's services in the shared requestList object
243
- // This caused unpredictable behavior and data corruption in production environments
244
- var loadScopedListClasses = function(context){
340
+ /**
341
+ * Load scoped service instances into request context
342
+ *
343
+ * CRITICAL FIX: Stores scoped services in the request-specific context instead of
344
+ * the shared requestList object. This prevents race conditions where concurrent
345
+ * requests would overwrite each other's services, causing unpredictable behavior
346
+ * and data corruption in production environments.
347
+ *
348
+ * @param {Object} context - Request-specific context object
349
+ * @returns {void}
350
+ *
351
+ * @example
352
+ * const requestContext = {};
353
+ * loadScopedListClasses.call(masterRouter, requestContext);
354
+ * // requestContext now has scoped service instances
355
+ */
356
+ const loadScopedListClasses = function(context){
245
357
  // FIXED: Use Object.entries() for safe iteration (prevents prototype pollution)
246
358
  for (const [key, className] of Object.entries(this._master._scopedList)) {
247
359
  // Store scoped services in the context object (request-specific) instead of shared requestList
@@ -250,11 +362,84 @@ var loadScopedListClasses = function(context){
250
362
  };
251
363
 
252
364
 
365
+ /**
366
+ * Validate route path format
367
+ *
368
+ * @param {string} path - Route path to validate
369
+ * @throws {Error} If path is invalid
370
+ */
371
+ function validateRoutePath(path) {
372
+ if (!path || typeof path !== 'string') {
373
+ throw new TypeError('Route path must be a non-empty string');
374
+ }
375
+
376
+ if (path.length > ROUTER_CONFIG.MAX_ROUTE_LENGTH) {
377
+ throw new Error(`Route path exceeds maximum length (${ROUTER_CONFIG.MAX_ROUTE_LENGTH} characters)`);
378
+ }
379
+
380
+ // Check for invalid characters
381
+ if (/[<>{}[\]\\^`|]/.test(path)) {
382
+ throw new Error(`Route path contains invalid characters: ${path}`);
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Validate HTTP method
388
+ *
389
+ * @param {string} method - HTTP method to validate
390
+ * @throws {Error} If method is invalid
391
+ */
392
+ function validateHttpMethod(method) {
393
+ const validMethods = Object.values(HTTP_METHODS);
394
+ if (!validMethods.includes(method.toLowerCase())) {
395
+ throw new Error(`Invalid HTTP method: ${method}. Must be one of: ${validMethods.join(', ')}`);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Validate controller/action name
401
+ *
402
+ * @param {string} name - Name to validate
403
+ * @param {string} type - Type (controller or action)
404
+ * @throws {Error} If name is invalid
405
+ */
406
+ function validateIdentifier(name, type) {
407
+ if (!name || typeof name !== 'string') {
408
+ throw new TypeError(`${type} name must be a non-empty string`);
409
+ }
410
+
411
+ // Controllers can have forward slashes for nested structures (e.g., "api/health")
412
+ // Actions must be simple identifiers
413
+ if (type === 'controller') {
414
+ // Split on slash and validate each segment
415
+ const segments = name.split('/');
416
+ for (const segment of segments) {
417
+ if (!segment || !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(segment)) {
418
+ throw new Error(`Invalid ${type} name: ${name}. Each segment must be a valid identifier.`);
419
+ }
420
+ }
421
+ } else {
422
+ // Actions must be simple identifiers (no slashes)
423
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
424
+ throw new Error(`Invalid ${type} name: ${name}. Must be a valid identifier.`);
425
+ }
426
+ }
427
+ }
428
+
253
429
  /**
254
430
  * Normalize route path: lowercase segments but preserve param names
255
431
  *
256
- * @param {String} path - Route path like "/Period/:periodId/Items/:itemId"
257
- * @returns {String} - Normalized: "period/:periodId/items/:itemId"
432
+ * Ensures consistent route matching by:
433
+ * - Converting path segments to lowercase
434
+ * - Preserving parameter names (segments starting with :)
435
+ * - Removing leading/trailing slashes
436
+ *
437
+ * @param {string} path - Route path like "/Period/:periodId/Items/:itemId"
438
+ * @returns {string} Normalized path: "period/:periodId/items/:itemId"
439
+ *
440
+ * @example
441
+ * normalizeRoutePath("/Users/:userId/Posts/:postId")
442
+ * // Returns: "users/:userId/posts/:postId"
258
443
  */
259
444
  function normalizeRoutePath(path) {
260
445
  const trimmed = path.replace(/^\/|\/$/g, '');
@@ -272,9 +457,22 @@ function normalizeRoutePath(path) {
272
457
  return normalized.join('/');
273
458
  }
274
459
 
460
+ /**
461
+ * MasterRouter - Route management and request routing
462
+ *
463
+ * Handles:
464
+ * - Route registration (manual and RESTful resources)
465
+ * - Path normalization and parameter extraction
466
+ * - Controller/action resolution and execution
467
+ * - Route constraints and middleware
468
+ * - Request-specific context isolation (no shared state)
469
+ *
470
+ * @class MasterRouter
471
+ */
275
472
  class MasterRouter {
276
473
  currentRouteName = null
277
474
  _routes = {}
475
+ _currentRoute = null // Instance property instead of global
278
476
 
279
477
  // Lazy-load master to avoid circular dependency (Google-style lazy initialization)
280
478
  get _master() {
@@ -284,18 +482,52 @@ class MasterRouter {
284
482
  return this.__masterCache;
285
483
  }
286
484
 
485
+ /**
486
+ * Start route definition builder
487
+ *
488
+ * Returns an object with methods for defining routes:
489
+ * - route(path, toPath, type, constraint): Define a single route
490
+ * - resources(routeName): Define RESTful resource routes
491
+ *
492
+ * @returns {Object} Route builder with route() and resources() methods
493
+ *
494
+ * @example
495
+ * const builder = masterRouter.start();
496
+ * builder.route("/users/:id", "users#show", "get");
497
+ * builder.resources("posts"); // Creates index, new, create, show, edit, update, destroy
498
+ */
287
499
  start(){
288
- var $that = this;
500
+ const $that = this;
289
501
  return {
290
502
  route : function(path, toPath, type, constraint){ // function to add to list of routes
503
+ // Input validation
504
+ validateRoutePath(path);
505
+ validateHttpMethod(type);
291
506
 
292
- var pathList = toPath.replace(/^\/|\/$/g, '').split("#");
507
+ if (!toPath || typeof toPath !== 'string') {
508
+ throw new TypeError('Route target (toPath) must be a non-empty string');
509
+ }
510
+
511
+ if (!/^[^#]+#[^#]+$/.test(toPath)) {
512
+ throw new Error(`Invalid route target format: ${toPath}. Must be "controller#action"`);
513
+ }
514
+
515
+ const pathList = toPath.replace(/^\/|\/$/g, '').split("#");
516
+ const controller = pathList[0].replace(/^\/|\/$/g, '');
517
+ const action = pathList[1];
518
+
519
+ validateIdentifier(controller, 'controller');
520
+ validateIdentifier(action, 'action');
293
521
 
294
- var route = {
522
+ if (constraint !== undefined && constraint !== null && typeof constraint !== 'function') {
523
+ throw new TypeError('Route constraint must be a function or null/undefined');
524
+ }
525
+
526
+ const route = {
295
527
  type: type.toLowerCase(),
296
528
  path: normalizeRoutePath(path),
297
- toController :pathList[0].replace(/^\/|\/$/g, ''),
298
- toAction: pathList[1],
529
+ toController: controller,
530
+ toAction: action,
299
531
  constraint : constraint
300
532
  };
301
533
 
@@ -304,10 +536,16 @@ class MasterRouter {
304
536
  },
305
537
 
306
538
  resources: function(routeName){ // function to add to list of routes using resources bulk
539
+ // Input validation
540
+ if (!routeName || typeof routeName !== 'string') {
541
+ throw new TypeError('Resource name must be a non-empty string');
542
+ }
307
543
 
544
+ validateIdentifier(routeName, 'resource');
545
+ validateRoutePath(`/${routeName}`);
308
546
 
309
547
  $that._routes[$that.currentRouteName].routes.push({
310
- type: "get",
548
+ type: HTTP_METHODS.GET,
311
549
  path: normalizeRoutePath(routeName),
312
550
  toController :routeName,
313
551
  toAction: "index",
@@ -315,7 +553,7 @@ class MasterRouter {
315
553
  });
316
554
 
317
555
  $that._routes[$that.currentRouteName].routes.push({
318
- type: "get",
556
+ type: HTTP_METHODS.GET,
319
557
  path: normalizeRoutePath(routeName),
320
558
  toController :routeName,
321
559
  toAction: "new",
@@ -323,7 +561,7 @@ class MasterRouter {
323
561
  });
324
562
 
325
563
  $that._routes[$that.currentRouteName].routes.push({
326
- type: "post",
564
+ type: HTTP_METHODS.POST,
327
565
  path: normalizeRoutePath(routeName),
328
566
  toController :routeName,
329
567
  toAction: "create",
@@ -332,7 +570,7 @@ class MasterRouter {
332
570
 
333
571
  $that._routes[$that.currentRouteName].routes.push({
334
572
  // pages/3
335
- type: "get",
573
+ type: HTTP_METHODS.GET,
336
574
  path: normalizeRoutePath(routeName + "/:id"),
337
575
  toController :routeName,
338
576
  toAction: "show",
@@ -340,7 +578,7 @@ class MasterRouter {
340
578
  });
341
579
 
342
580
  $that._routes[$that.currentRouteName].routes.push({
343
- type: "get",
581
+ type: HTTP_METHODS.GET,
344
582
  path: normalizeRoutePath(routeName + "/:id/edit"),
345
583
  toController :routeName,
346
584
  toAction: "edit",
@@ -348,7 +586,7 @@ class MasterRouter {
348
586
  });
349
587
 
350
588
  $that._routes[$that.currentRouteName].routes.push({
351
- type: "put",
589
+ type: HTTP_METHODS.PUT,
352
590
  path: normalizeRoutePath(routeName + "/:id"),
353
591
  toController :routeName,
354
592
  toAction: "update",
@@ -356,7 +594,7 @@ class MasterRouter {
356
594
  });
357
595
 
358
596
  $that._routes[$that.currentRouteName].routes.push({
359
- type: "delete",
597
+ type: HTTP_METHODS.DELETE,
360
598
  path: normalizeRoutePath(routeName + "/:id"),
361
599
  toController :routeName,
362
600
  toAction: "destroy",
@@ -366,17 +604,61 @@ class MasterRouter {
366
604
  }
367
605
  }
368
606
 
607
+ /**
608
+ * Initialize router with MIME type list
609
+ *
610
+ * @param {Object} mimeList - Object mapping file extensions to MIME types
611
+ * @returns {void}
612
+ * @deprecated Use addMimeList() instead
613
+ *
614
+ * @example
615
+ * router.loadRoutes({ json: 'application/json', html: 'text/html' });
616
+ */
369
617
  loadRoutes(mimeList){
370
618
  this.init(mimeList);
371
619
  }
372
620
 
621
+ /**
622
+ * Add MIME type mappings
623
+ *
624
+ * @param {Object} mimeList - Object mapping file extensions to MIME types
625
+ * @returns {void}
626
+ *
627
+ * @example
628
+ * router.addMimeList({ json: 'application/json', xml: 'application/xml' });
629
+ */
373
630
  addMimeList(mimeList){
374
631
  this._addMimeList(mimeList);
375
632
  }
376
633
 
634
+ /**
635
+ * Setup a new route scope
636
+ *
637
+ * Creates a new route group with a unique ID. All routes defined via start()
638
+ * will be added to this scope until setup() is called again.
639
+ *
640
+ * @param {Object} route - Route scope configuration
641
+ * @param {string} route.root - Application root path
642
+ * @param {boolean} [route.isComponent=false] - Whether this is a component route
643
+ * @returns {void}
644
+ *
645
+ * @example
646
+ * router.setup({ root: '/app', isComponent: false });
647
+ * const builder = router.start();
648
+ * builder.route("/users", "users#index", "get");
649
+ */
377
650
  setup(route){
378
- this.currentRouteName = tools.makeWordId(4);
379
-
651
+ // Input validation
652
+ if (!route || typeof route !== 'object') {
653
+ throw new TypeError('Route configuration must be an object');
654
+ }
655
+
656
+ if (!route.root || typeof route.root !== 'string') {
657
+ throw new TypeError('Route configuration must have a valid root path');
658
+ }
659
+
660
+ this.currentRouteName = tools.makeWordId(ROUTER_CONFIG.ROUTE_ID_LENGTH);
661
+
380
662
  if(this._routes[this.currentRouteName] === undefined){
381
663
  this._routes[this.currentRouteName] = {
382
664
  root : route.root,
@@ -386,21 +668,44 @@ class MasterRouter {
386
668
  }
387
669
  }
388
670
 
671
+ /**
672
+ * Get current route (deprecated - use requestObject.currentRoute instead)
673
+ * @deprecated Store route in requestObject.currentRoute for request isolation
674
+ * @returns {Object} Current route information
675
+ */
389
676
  get currentRoute(){
390
- return currentRoute;
677
+ return this._currentRoute;
391
678
  }
392
679
 
680
+ /**
681
+ * Set current route (deprecated - use requestObject.currentRoute instead)
682
+ * @deprecated Store route in requestObject.currentRoute for request isolation
683
+ * @param {Object} data - Route data
684
+ */
393
685
  set currentRoute(data){
394
- currentRoute = data;
686
+ this._currentRoute = data;
395
687
  }
396
688
 
397
689
  _addMimeList(mimeObject){
398
- var that = this;
690
+ const that = this;
399
691
  if(mimeObject){
400
692
  that.mimeTypes = mimeObject;
401
693
  }
402
694
  }
403
695
 
696
+ /**
697
+ * Find MIME type for file extension
698
+ *
699
+ * Performs O(1) constant-time lookup in MIME types object.
700
+ *
701
+ * @param {string} fileExt - File extension (with or without leading dot)
702
+ * @returns {string|boolean} MIME type string or false if not found
703
+ *
704
+ * @example
705
+ * router.findMimeType("json") // Returns: "application/json"
706
+ * router.findMimeType(".html") // Returns: "text/html"
707
+ * router.findMimeType("unknown") // Returns: false
708
+ */
404
709
  findMimeType(fileExt){
405
710
  if(!fileExt){
406
711
  return false;
@@ -417,18 +722,39 @@ class MasterRouter {
417
722
  return type || false;
418
723
  }
419
724
 
725
+ /**
726
+ * Execute controller action for matched route
727
+ *
728
+ * Internal method that:
729
+ * 1. Loads the controller file
730
+ * 2. Creates controller instance with request context
731
+ * 3. Executes beforeAction filters
732
+ * 4. Calls the action method
733
+ * 5. Handles errors and sends responses
734
+ *
735
+ * @private
736
+ * @param {Object} requestObject - Request context with route information
737
+ * @returns {void}
738
+ *
739
+ * @example
740
+ * // Called internally when route matches
741
+ * this._call(requestObject);
742
+ */
420
743
  _call(requestObject){
421
744
 
422
745
  // Start performance tracking
423
746
  const requestId = `${Date.now()}-${Math.random()}`;
424
747
  performanceTracker.start(requestId, requestObject);
425
748
 
749
+ // CRITICAL FIX: Use currentRoute from requestObject (not global)
750
+ const currentRoute = requestObject.currentRoute;
751
+
426
752
  // CRITICAL FIX: Create a request-specific context instead of using shared requestList
427
753
  // This prevents race conditions where concurrent requests overwrite each other's services
428
754
  const requestContext = Object.create(this._master.requestList);
429
755
  tools.combineObjects(requestContext, requestObject);
430
756
  requestObject = requestContext;
431
- var Control = null;
757
+ let Control = null;
432
758
 
433
759
  try{
434
760
  // Try to load controller
@@ -459,10 +785,10 @@ class MasterRouter {
459
785
  Control.prototype.__currentRoute = currentRoute;
460
786
  Control.prototype.__response = requestObject.response;
461
787
  Control.prototype.__request = requestObject.request;
462
- var control = new Control(requestObject);
463
- var _callEmit = new EventEmitter();
788
+ const control = new Control(requestObject);
789
+ const _callEmit = new EventEmitter();
464
790
 
465
- _callEmit.on("controller", function(){
791
+ _callEmit.on(EVENT_NAMES.CONTROLLER, function(){
466
792
  try {
467
793
  control.next = function(){
468
794
  control.__callAfterAction(control, requestObject);
@@ -484,6 +810,8 @@ class MasterRouter {
484
810
  Promise.resolve(wrappedAction.call(control, requestObject))
485
811
  .then(() => {
486
812
  performanceTracker.end(requestId);
813
+ // MEMORY LEAK FIX: Clean up event listeners
814
+ _callEmit.removeAllListeners();
487
815
  })
488
816
  .catch((error) => {
489
817
  const mcError = handleControllerError(
@@ -495,6 +823,8 @@ class MasterRouter {
495
823
  );
496
824
  sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
497
825
  performanceTracker.end(requestId);
826
+ // MEMORY LEAK FIX: Clean up event listeners
827
+ _callEmit.removeAllListeners();
498
828
  });
499
829
 
500
830
  } catch (error) {
@@ -508,6 +838,8 @@ class MasterRouter {
508
838
  );
509
839
  sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
510
840
  performanceTracker.end(requestId);
841
+ // MEMORY LEAK FIX: Clean up event listeners
842
+ _callEmit.removeAllListeners();
511
843
  }
512
844
  });
513
845
 
@@ -533,26 +865,75 @@ class MasterRouter {
533
865
 
534
866
  }
535
867
 
868
+ /**
869
+ * Load and route incoming request
870
+ *
871
+ * Main entry point for request routing:
872
+ * 1. Creates request-specific context
873
+ * 2. Loads scoped services
874
+ * 3. Normalizes request path
875
+ * 4. Searches for matching route
876
+ * 5. Triggers controller execution or sends 404
877
+ *
878
+ * @param {Object} rr - Raw request object
879
+ * @param {Object} rr.request - HTTP request
880
+ * @param {Object} rr.response - HTTP response
881
+ * @param {string} rr.pathName - Request path
882
+ * @param {string} rr.type - HTTP method (get, post, etc.)
883
+ * @param {Object} [rr.params={}] - Query parameters
884
+ * @returns {void}
885
+ *
886
+ * @example
887
+ * router.load({
888
+ * request: req,
889
+ * response: res,
890
+ * pathName: "/users/123",
891
+ * type: "get",
892
+ * params: {}
893
+ * });
894
+ */
536
895
  load(rr){ // load the the router
896
+ // Input validation
897
+ if (!rr || typeof rr !== 'object') {
898
+ throw new TypeError('Request object must be a valid object');
899
+ }
900
+
901
+ if (!rr.request || typeof rr.request !== 'object') {
902
+ throw new TypeError('Request object must have a valid request property');
903
+ }
904
+
905
+ if (!rr.response || typeof rr.response !== 'object') {
906
+ throw new TypeError('Request object must have a valid response property');
907
+ }
537
908
 
538
- var $that = this;
539
- var requestObject = Object.create(rr);
909
+ if (!rr.pathName || typeof rr.pathName !== 'string') {
910
+ throw new TypeError('Request object must have a valid pathName');
911
+ }
912
+
913
+ if (!rr.type || typeof rr.type !== 'string') {
914
+ throw new TypeError('Request object must have a valid type (HTTP method)');
915
+ }
916
+
917
+ const $that = this;
918
+ const requestObject = Object.create(rr);
540
919
 
541
920
  // CRITICAL FIX: Load scoped services into request-specific context
542
921
  // Pass requestObject so scoped services are stored per-request, not globally
543
922
  loadScopedListClasses.call(this, requestObject);
544
923
  requestObject.pathName = requestObject.pathName.replace(/^\/|\/$/g, '').toLowerCase();
545
-
546
- var _loadEmit = new EventEmitter();
547
-
548
- _loadEmit.on("routeConstraintGood", function(requestObj){
924
+
925
+ const _loadEmit = new EventEmitter();
926
+
927
+ _loadEmit.on(EVENT_NAMES.ROUTE_CONSTRAINT_GOOD, function(requestObj){
549
928
  $that._call(requestObj);
929
+ // MEMORY LEAK FIX: Clean up event listeners after handling route
930
+ _loadEmit.removeAllListeners();
550
931
  });
551
-
552
- var routeFound = false;
932
+
933
+ let routeFound = false;
553
934
  const routes = Object.keys(this._routes);
554
935
  for (const route of routes) {
555
- var result = processRoutes(requestObject, _loadEmit, this._routes[route] );
936
+ const result = processRoutes(requestObject, _loadEmit, this._routes[route] );
556
937
  if(result === true){
557
938
  routeFound = true;
558
939
  break;
@@ -570,7 +951,9 @@ class MasterRouter {
570
951
 
571
952
  const mcError = handleRoutingError(requestObject.pathName, allRoutes);
572
953
  sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
573
- }
954
+ // MEMORY LEAK FIX: Clean up event listeners if route not found
955
+ _loadEmit.removeAllListeners();
956
+ }
574
957
 
575
958
  }
576
959