mastercontroller 1.3.13 → 1.3.15

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,72 @@ 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
+ // Must be valid JavaScript identifier
412
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
413
+ throw new Error(`Invalid ${type} name: ${name}. Must be a valid identifier.`);
414
+ }
415
+ }
416
+
253
417
  /**
254
418
  * Normalize route path: lowercase segments but preserve param names
255
419
  *
256
- * @param {String} path - Route path like "/Period/:periodId/Items/:itemId"
257
- * @returns {String} - Normalized: "period/:periodId/items/:itemId"
420
+ * Ensures consistent route matching by:
421
+ * - Converting path segments to lowercase
422
+ * - Preserving parameter names (segments starting with :)
423
+ * - Removing leading/trailing slashes
424
+ *
425
+ * @param {string} path - Route path like "/Period/:periodId/Items/:itemId"
426
+ * @returns {string} Normalized path: "period/:periodId/items/:itemId"
427
+ *
428
+ * @example
429
+ * normalizeRoutePath("/Users/:userId/Posts/:postId")
430
+ * // Returns: "users/:userId/posts/:postId"
258
431
  */
259
432
  function normalizeRoutePath(path) {
260
433
  const trimmed = path.replace(/^\/|\/$/g, '');
@@ -272,9 +445,22 @@ function normalizeRoutePath(path) {
272
445
  return normalized.join('/');
273
446
  }
274
447
 
448
+ /**
449
+ * MasterRouter - Route management and request routing
450
+ *
451
+ * Handles:
452
+ * - Route registration (manual and RESTful resources)
453
+ * - Path normalization and parameter extraction
454
+ * - Controller/action resolution and execution
455
+ * - Route constraints and middleware
456
+ * - Request-specific context isolation (no shared state)
457
+ *
458
+ * @class MasterRouter
459
+ */
275
460
  class MasterRouter {
276
461
  currentRouteName = null
277
462
  _routes = {}
463
+ _currentRoute = null // Instance property instead of global
278
464
 
279
465
  // Lazy-load master to avoid circular dependency (Google-style lazy initialization)
280
466
  get _master() {
@@ -284,18 +470,52 @@ class MasterRouter {
284
470
  return this.__masterCache;
285
471
  }
286
472
 
473
+ /**
474
+ * Start route definition builder
475
+ *
476
+ * Returns an object with methods for defining routes:
477
+ * - route(path, toPath, type, constraint): Define a single route
478
+ * - resources(routeName): Define RESTful resource routes
479
+ *
480
+ * @returns {Object} Route builder with route() and resources() methods
481
+ *
482
+ * @example
483
+ * const builder = masterRouter.start();
484
+ * builder.route("/users/:id", "users#show", "get");
485
+ * builder.resources("posts"); // Creates index, new, create, show, edit, update, destroy
486
+ */
287
487
  start(){
288
- var $that = this;
488
+ const $that = this;
289
489
  return {
290
490
  route : function(path, toPath, type, constraint){ // function to add to list of routes
491
+ // Input validation
492
+ validateRoutePath(path);
493
+ validateHttpMethod(type);
494
+
495
+ if (!toPath || typeof toPath !== 'string') {
496
+ throw new TypeError('Route target (toPath) must be a non-empty string');
497
+ }
498
+
499
+ if (!/^[^#]+#[^#]+$/.test(toPath)) {
500
+ throw new Error(`Invalid route target format: ${toPath}. Must be "controller#action"`);
501
+ }
502
+
503
+ const pathList = toPath.replace(/^\/|\/$/g, '').split("#");
504
+ const controller = pathList[0].replace(/^\/|\/$/g, '');
505
+ const action = pathList[1];
506
+
507
+ validateIdentifier(controller, 'controller');
508
+ validateIdentifier(action, 'action');
291
509
 
292
- var pathList = toPath.replace(/^\/|\/$/g, '').split("#");
510
+ if (constraint !== undefined && constraint !== null && typeof constraint !== 'function') {
511
+ throw new TypeError('Route constraint must be a function or null/undefined');
512
+ }
293
513
 
294
- var route = {
514
+ const route = {
295
515
  type: type.toLowerCase(),
296
516
  path: normalizeRoutePath(path),
297
- toController :pathList[0].replace(/^\/|\/$/g, ''),
298
- toAction: pathList[1],
517
+ toController: controller,
518
+ toAction: action,
299
519
  constraint : constraint
300
520
  };
301
521
 
@@ -304,10 +524,16 @@ class MasterRouter {
304
524
  },
305
525
 
306
526
  resources: function(routeName){ // function to add to list of routes using resources bulk
527
+ // Input validation
528
+ if (!routeName || typeof routeName !== 'string') {
529
+ throw new TypeError('Resource name must be a non-empty string');
530
+ }
307
531
 
532
+ validateIdentifier(routeName, 'resource');
533
+ validateRoutePath(`/${routeName}`);
308
534
 
309
535
  $that._routes[$that.currentRouteName].routes.push({
310
- type: "get",
536
+ type: HTTP_METHODS.GET,
311
537
  path: normalizeRoutePath(routeName),
312
538
  toController :routeName,
313
539
  toAction: "index",
@@ -315,7 +541,7 @@ class MasterRouter {
315
541
  });
316
542
 
317
543
  $that._routes[$that.currentRouteName].routes.push({
318
- type: "get",
544
+ type: HTTP_METHODS.GET,
319
545
  path: normalizeRoutePath(routeName),
320
546
  toController :routeName,
321
547
  toAction: "new",
@@ -323,7 +549,7 @@ class MasterRouter {
323
549
  });
324
550
 
325
551
  $that._routes[$that.currentRouteName].routes.push({
326
- type: "post",
552
+ type: HTTP_METHODS.POST,
327
553
  path: normalizeRoutePath(routeName),
328
554
  toController :routeName,
329
555
  toAction: "create",
@@ -332,7 +558,7 @@ class MasterRouter {
332
558
 
333
559
  $that._routes[$that.currentRouteName].routes.push({
334
560
  // pages/3
335
- type: "get",
561
+ type: HTTP_METHODS.GET,
336
562
  path: normalizeRoutePath(routeName + "/:id"),
337
563
  toController :routeName,
338
564
  toAction: "show",
@@ -340,7 +566,7 @@ class MasterRouter {
340
566
  });
341
567
 
342
568
  $that._routes[$that.currentRouteName].routes.push({
343
- type: "get",
569
+ type: HTTP_METHODS.GET,
344
570
  path: normalizeRoutePath(routeName + "/:id/edit"),
345
571
  toController :routeName,
346
572
  toAction: "edit",
@@ -348,7 +574,7 @@ class MasterRouter {
348
574
  });
349
575
 
350
576
  $that._routes[$that.currentRouteName].routes.push({
351
- type: "put",
577
+ type: HTTP_METHODS.PUT,
352
578
  path: normalizeRoutePath(routeName + "/:id"),
353
579
  toController :routeName,
354
580
  toAction: "update",
@@ -356,7 +582,7 @@ class MasterRouter {
356
582
  });
357
583
 
358
584
  $that._routes[$that.currentRouteName].routes.push({
359
- type: "delete",
585
+ type: HTTP_METHODS.DELETE,
360
586
  path: normalizeRoutePath(routeName + "/:id"),
361
587
  toController :routeName,
362
588
  toAction: "destroy",
@@ -366,17 +592,61 @@ class MasterRouter {
366
592
  }
367
593
  }
368
594
 
595
+ /**
596
+ * Initialize router with MIME type list
597
+ *
598
+ * @param {Object} mimeList - Object mapping file extensions to MIME types
599
+ * @returns {void}
600
+ * @deprecated Use addMimeList() instead
601
+ *
602
+ * @example
603
+ * router.loadRoutes({ json: 'application/json', html: 'text/html' });
604
+ */
369
605
  loadRoutes(mimeList){
370
606
  this.init(mimeList);
371
607
  }
372
608
 
609
+ /**
610
+ * Add MIME type mappings
611
+ *
612
+ * @param {Object} mimeList - Object mapping file extensions to MIME types
613
+ * @returns {void}
614
+ *
615
+ * @example
616
+ * router.addMimeList({ json: 'application/json', xml: 'application/xml' });
617
+ */
373
618
  addMimeList(mimeList){
374
619
  this._addMimeList(mimeList);
375
620
  }
376
621
 
622
+ /**
623
+ * Setup a new route scope
624
+ *
625
+ * Creates a new route group with a unique ID. All routes defined via start()
626
+ * will be added to this scope until setup() is called again.
627
+ *
628
+ * @param {Object} route - Route scope configuration
629
+ * @param {string} route.root - Application root path
630
+ * @param {boolean} [route.isComponent=false] - Whether this is a component route
631
+ * @returns {void}
632
+ *
633
+ * @example
634
+ * router.setup({ root: '/app', isComponent: false });
635
+ * const builder = router.start();
636
+ * builder.route("/users", "users#index", "get");
637
+ */
377
638
  setup(route){
378
- this.currentRouteName = tools.makeWordId(4);
379
-
639
+ // Input validation
640
+ if (!route || typeof route !== 'object') {
641
+ throw new TypeError('Route configuration must be an object');
642
+ }
643
+
644
+ if (!route.root || typeof route.root !== 'string') {
645
+ throw new TypeError('Route configuration must have a valid root path');
646
+ }
647
+
648
+ this.currentRouteName = tools.makeWordId(ROUTER_CONFIG.ROUTE_ID_LENGTH);
649
+
380
650
  if(this._routes[this.currentRouteName] === undefined){
381
651
  this._routes[this.currentRouteName] = {
382
652
  root : route.root,
@@ -386,21 +656,44 @@ class MasterRouter {
386
656
  }
387
657
  }
388
658
 
659
+ /**
660
+ * Get current route (deprecated - use requestObject.currentRoute instead)
661
+ * @deprecated Store route in requestObject.currentRoute for request isolation
662
+ * @returns {Object} Current route information
663
+ */
389
664
  get currentRoute(){
390
- return currentRoute;
665
+ return this._currentRoute;
391
666
  }
392
667
 
668
+ /**
669
+ * Set current route (deprecated - use requestObject.currentRoute instead)
670
+ * @deprecated Store route in requestObject.currentRoute for request isolation
671
+ * @param {Object} data - Route data
672
+ */
393
673
  set currentRoute(data){
394
- currentRoute = data;
674
+ this._currentRoute = data;
395
675
  }
396
676
 
397
677
  _addMimeList(mimeObject){
398
- var that = this;
678
+ const that = this;
399
679
  if(mimeObject){
400
680
  that.mimeTypes = mimeObject;
401
681
  }
402
682
  }
403
683
 
684
+ /**
685
+ * Find MIME type for file extension
686
+ *
687
+ * Performs O(1) constant-time lookup in MIME types object.
688
+ *
689
+ * @param {string} fileExt - File extension (with or without leading dot)
690
+ * @returns {string|boolean} MIME type string or false if not found
691
+ *
692
+ * @example
693
+ * router.findMimeType("json") // Returns: "application/json"
694
+ * router.findMimeType(".html") // Returns: "text/html"
695
+ * router.findMimeType("unknown") // Returns: false
696
+ */
404
697
  findMimeType(fileExt){
405
698
  if(!fileExt){
406
699
  return false;
@@ -417,18 +710,39 @@ class MasterRouter {
417
710
  return type || false;
418
711
  }
419
712
 
713
+ /**
714
+ * Execute controller action for matched route
715
+ *
716
+ * Internal method that:
717
+ * 1. Loads the controller file
718
+ * 2. Creates controller instance with request context
719
+ * 3. Executes beforeAction filters
720
+ * 4. Calls the action method
721
+ * 5. Handles errors and sends responses
722
+ *
723
+ * @private
724
+ * @param {Object} requestObject - Request context with route information
725
+ * @returns {void}
726
+ *
727
+ * @example
728
+ * // Called internally when route matches
729
+ * this._call(requestObject);
730
+ */
420
731
  _call(requestObject){
421
732
 
422
733
  // Start performance tracking
423
734
  const requestId = `${Date.now()}-${Math.random()}`;
424
735
  performanceTracker.start(requestId, requestObject);
425
736
 
737
+ // CRITICAL FIX: Use currentRoute from requestObject (not global)
738
+ const currentRoute = requestObject.currentRoute;
739
+
426
740
  // CRITICAL FIX: Create a request-specific context instead of using shared requestList
427
741
  // This prevents race conditions where concurrent requests overwrite each other's services
428
742
  const requestContext = Object.create(this._master.requestList);
429
743
  tools.combineObjects(requestContext, requestObject);
430
744
  requestObject = requestContext;
431
- var Control = null;
745
+ let Control = null;
432
746
 
433
747
  try{
434
748
  // Try to load controller
@@ -459,10 +773,10 @@ class MasterRouter {
459
773
  Control.prototype.__currentRoute = currentRoute;
460
774
  Control.prototype.__response = requestObject.response;
461
775
  Control.prototype.__request = requestObject.request;
462
- var control = new Control(requestObject);
463
- var _callEmit = new EventEmitter();
776
+ const control = new Control(requestObject);
777
+ const _callEmit = new EventEmitter();
464
778
 
465
- _callEmit.on("controller", function(){
779
+ _callEmit.on(EVENT_NAMES.CONTROLLER, function(){
466
780
  try {
467
781
  control.next = function(){
468
782
  control.__callAfterAction(control, requestObject);
@@ -484,6 +798,8 @@ class MasterRouter {
484
798
  Promise.resolve(wrappedAction.call(control, requestObject))
485
799
  .then(() => {
486
800
  performanceTracker.end(requestId);
801
+ // MEMORY LEAK FIX: Clean up event listeners
802
+ _callEmit.removeAllListeners();
487
803
  })
488
804
  .catch((error) => {
489
805
  const mcError = handleControllerError(
@@ -495,6 +811,8 @@ class MasterRouter {
495
811
  );
496
812
  sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
497
813
  performanceTracker.end(requestId);
814
+ // MEMORY LEAK FIX: Clean up event listeners
815
+ _callEmit.removeAllListeners();
498
816
  });
499
817
 
500
818
  } catch (error) {
@@ -508,6 +826,8 @@ class MasterRouter {
508
826
  );
509
827
  sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
510
828
  performanceTracker.end(requestId);
829
+ // MEMORY LEAK FIX: Clean up event listeners
830
+ _callEmit.removeAllListeners();
511
831
  }
512
832
  });
513
833
 
@@ -533,26 +853,75 @@ class MasterRouter {
533
853
 
534
854
  }
535
855
 
856
+ /**
857
+ * Load and route incoming request
858
+ *
859
+ * Main entry point for request routing:
860
+ * 1. Creates request-specific context
861
+ * 2. Loads scoped services
862
+ * 3. Normalizes request path
863
+ * 4. Searches for matching route
864
+ * 5. Triggers controller execution or sends 404
865
+ *
866
+ * @param {Object} rr - Raw request object
867
+ * @param {Object} rr.request - HTTP request
868
+ * @param {Object} rr.response - HTTP response
869
+ * @param {string} rr.pathName - Request path
870
+ * @param {string} rr.type - HTTP method (get, post, etc.)
871
+ * @param {Object} [rr.params={}] - Query parameters
872
+ * @returns {void}
873
+ *
874
+ * @example
875
+ * router.load({
876
+ * request: req,
877
+ * response: res,
878
+ * pathName: "/users/123",
879
+ * type: "get",
880
+ * params: {}
881
+ * });
882
+ */
536
883
  load(rr){ // load the the router
884
+ // Input validation
885
+ if (!rr || typeof rr !== 'object') {
886
+ throw new TypeError('Request object must be a valid object');
887
+ }
537
888
 
538
- var $that = this;
539
- var requestObject = Object.create(rr);
889
+ if (!rr.request || typeof rr.request !== 'object') {
890
+ throw new TypeError('Request object must have a valid request property');
891
+ }
892
+
893
+ if (!rr.response || typeof rr.response !== 'object') {
894
+ throw new TypeError('Request object must have a valid response property');
895
+ }
896
+
897
+ if (!rr.pathName || typeof rr.pathName !== 'string') {
898
+ throw new TypeError('Request object must have a valid pathName');
899
+ }
900
+
901
+ if (!rr.type || typeof rr.type !== 'string') {
902
+ throw new TypeError('Request object must have a valid type (HTTP method)');
903
+ }
904
+
905
+ const $that = this;
906
+ const requestObject = Object.create(rr);
540
907
 
541
908
  // CRITICAL FIX: Load scoped services into request-specific context
542
909
  // Pass requestObject so scoped services are stored per-request, not globally
543
910
  loadScopedListClasses.call(this, requestObject);
544
911
  requestObject.pathName = requestObject.pathName.replace(/^\/|\/$/g, '').toLowerCase();
545
-
546
- var _loadEmit = new EventEmitter();
547
-
548
- _loadEmit.on("routeConstraintGood", function(requestObj){
912
+
913
+ const _loadEmit = new EventEmitter();
914
+
915
+ _loadEmit.on(EVENT_NAMES.ROUTE_CONSTRAINT_GOOD, function(requestObj){
549
916
  $that._call(requestObj);
917
+ // MEMORY LEAK FIX: Clean up event listeners after handling route
918
+ _loadEmit.removeAllListeners();
550
919
  });
551
-
552
- var routeFound = false;
920
+
921
+ let routeFound = false;
553
922
  const routes = Object.keys(this._routes);
554
923
  for (const route of routes) {
555
- var result = processRoutes(requestObject, _loadEmit, this._routes[route] );
924
+ const result = processRoutes(requestObject, _loadEmit, this._routes[route] );
556
925
  if(result === true){
557
926
  routeFound = true;
558
927
  break;
@@ -570,7 +939,9 @@ class MasterRouter {
570
939
 
571
940
  const mcError = handleRoutingError(requestObject.pathName, allRoutes);
572
941
  sendErrorResponse(requestObject.response, mcError, requestObject.pathName);
573
- }
942
+ // MEMORY LEAK FIX: Clean up event listeners if route not found
943
+ _loadEmit.removeAllListeners();
944
+ }
574
945
 
575
946
  }
576
947