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.
@@ -1,8 +1,19 @@
1
1
  // version 2.0 - FIXED: Instance-level filters, async support, multiple filters
2
2
  const { logger } = require('./error/MasterErrorLogger');
3
3
 
4
+ // HTTP Status Code Constants
5
+ const HTTP_STATUS = {
6
+ OK: 200,
7
+ BAD_REQUEST: 400,
8
+ FORBIDDEN: 403,
9
+ INTERNAL_ERROR: 500
10
+ };
11
+
4
12
  class MasterActionFilters {
5
13
 
14
+ // Default filter timeout (5 seconds)
15
+ static DEFAULT_FILTER_TIMEOUT = 5000;
16
+
6
17
  // Lazy-load master to avoid circular dependency (Google-style Singleton pattern)
7
18
  static get _master() {
8
19
  if (!MasterActionFilters.__masterCache) {
@@ -16,83 +27,256 @@ class MasterActionFilters {
16
27
  // Each controller gets its own filter arrays
17
28
  this._beforeActionFilters = [];
18
29
  this._afterActionFilters = [];
30
+
31
+ // Configurable timeout per controller instance
32
+ this._filterTimeout = MasterActionFilters.DEFAULT_FILTER_TIMEOUT;
33
+ }
34
+
35
+ /**
36
+ * Normalize action name by removing whitespace
37
+ * @private
38
+ * @param {string} action - Action name
39
+ * @returns {string} Normalized action name
40
+ */
41
+ _normalizeAction(action) {
42
+ return action.replace(/\s/g, '');
19
43
  }
20
44
 
21
- // Register a before action filter
22
- // FIXED: Adds to array instead of overwriting
23
- beforeAction(actionlist, func){
45
+ /**
46
+ * Find matching filters for an action
47
+ * @private
48
+ * @param {Array} filters - Filter array to search
49
+ * @param {Object} obj - Controller instance
50
+ * @param {Object} request - Request object
51
+ * @returns {Array} Matching filters
52
+ */
53
+ _findMatchingFilters(filters, obj, request) {
54
+ if (!filters || filters.length === 0) {
55
+ return [];
56
+ }
57
+
58
+ const requestAction = this._normalizeAction(request.toAction);
59
+
60
+ return filters.filter(filter => {
61
+ // Skip disabled filters
62
+ if (filter.enabled === false) {
63
+ return false;
64
+ }
65
+
66
+ // Check namespace matches
67
+ if (filter.namespace !== obj.__namespace) {
68
+ return false;
69
+ }
70
+
71
+ // Check if any action in filter's actionList matches
72
+ return filter.actionList.some(actionName => {
73
+ const normalizedAction = this._normalizeAction(actionName);
74
+ return normalizedAction === requestAction;
75
+ });
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Send standardized error response for filter failures
81
+ * @private
82
+ * @param {Object} request - Request object
83
+ * @param {number} statusCode - HTTP status code
84
+ * @param {string} errorCode - Error code for client
85
+ * @returns {void}
86
+ */
87
+ _sendFilterErrorResponse(request, statusCode, errorCode) {
88
+ const res = request.response;
89
+ if (res && !res._headerSent && !res.headersSent) {
90
+ const errorResponse = {
91
+ error: true,
92
+ statusCode,
93
+ code: errorCode,
94
+ message: 'Filter execution failed',
95
+ timestamp: new Date().toISOString(),
96
+ path: request.pathName,
97
+ method: request.request?.method
98
+ };
99
+
100
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
101
+ res.end(JSON.stringify(errorResponse));
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Validate filter registration parameters
107
+ * @private
108
+ * @param {string|Array<string>} actionlist - Action names to filter
109
+ * @param {Function} func - Filter callback function
110
+ * @param {string} filterType - 'before' or 'after' for logging
111
+ * @returns {boolean} True if valid, false otherwise
112
+ */
113
+ _validateFilterParams(actionlist, func, filterType) {
114
+ // Validate callback function
24
115
  if (typeof func !== 'function') {
25
- MasterActionFilters._master.error.log("beforeAction callback not a function", "warn");
116
+ logger.warn({
117
+ code: 'MC_FILTER_INVALID_CALLBACK',
118
+ message: `${filterType}Action callback is not a function`,
119
+ namespace: this.__namespace
120
+ });
121
+ return false;
122
+ }
123
+
124
+ // Validate actionlist exists
125
+ if (!actionlist || (Array.isArray(actionlist) && actionlist.length === 0)) {
126
+ logger.warn({
127
+ code: 'MC_FILTER_EMPTY_ACTIONLIST',
128
+ message: `${filterType}Action actionlist is empty or undefined`,
129
+ namespace: this.__namespace
130
+ });
131
+ return false;
132
+ }
133
+
134
+ // Validate actionlist contains valid strings
135
+ const actions = Array.isArray(actionlist) ? actionlist : [actionlist];
136
+ for (const action of actions) {
137
+ if (typeof action !== 'string' || action.trim() === '') {
138
+ logger.warn({
139
+ code: 'MC_FILTER_INVALID_ACTION',
140
+ message: `${filterType}Action actionlist contains invalid action name`,
141
+ namespace: this.__namespace,
142
+ action: action
143
+ });
144
+ return false;
145
+ }
146
+ }
147
+
148
+ // Validate namespace exists
149
+ if (!this.__namespace) {
150
+ logger.warn({
151
+ code: 'MC_FILTER_MISSING_NAMESPACE',
152
+ message: `${filterType}Action called but controller has no __namespace`,
153
+ });
154
+ return false;
155
+ }
156
+
157
+ return true;
158
+ }
159
+
160
+ /**
161
+ * Register a before action filter
162
+ * Filters execute before the specified actions in priority order (higher priority first)
163
+ * @param {string|Array<string>} actionlist - Action name(s) to filter
164
+ * @param {Function} func - Filter callback function(request)
165
+ * @param {Object} [options={}] - Filter options
166
+ * @param {number} [options.priority=0] - Filter priority (higher executes first)
167
+ * @param {string} [options.name] - Filter name for debugging
168
+ * @returns {void}
169
+ * @example
170
+ * this.beforeAction('index', function(request) {
171
+ * if (!this.isAuthenticated()) {
172
+ * this.redirectTo('/login');
173
+ * return;
174
+ * }
175
+ * this.next(); // Continue to action
176
+ * });
177
+ * @example
178
+ * // With priority (higher priority runs first)
179
+ * this.beforeAction(['create', 'update'], function(request) {
180
+ * this.loadUser();
181
+ * this.next();
182
+ * }, { priority: 10, name: 'loadUser' });
183
+ */
184
+ beforeAction(actionlist, func, options = {}){
185
+ if (!this._validateFilterParams(actionlist, func, 'before')) {
26
186
  return;
27
187
  }
28
188
 
29
- // FIXED: Push to array, don't overwrite
189
+ // FIXED: Push to array with metadata and priority
30
190
  this._beforeActionFilters.push({
31
191
  namespace: this.__namespace,
32
192
  actionList: Array.isArray(actionlist) ? actionlist : [actionlist],
33
193
  callBack: func,
34
- that: this
194
+ that: this,
195
+ priority: options.priority || 0,
196
+ name: options.name || func.name || 'anonymous',
197
+ enabled: true,
198
+ registeredAt: new Date().toISOString()
35
199
  });
200
+
201
+ // Sort filters by priority (higher priority first)
202
+ this._beforeActionFilters.sort((a, b) => b.priority - a.priority);
36
203
  }
37
204
 
38
- // Register an after action filter
39
- // FIXED: Adds to array instead of overwriting
40
- afterAction(actionlist, func){
41
- if (typeof func !== 'function') {
42
- MasterActionFilters._master.error.log("afterAction callback not a function", "warn");
205
+ /**
206
+ * Register an after action filter
207
+ * Filters execute after the specified actions complete in priority order
208
+ * @param {string|Array<string>} actionlist - Action name(s) to filter
209
+ * @param {Function} func - Filter callback function(request)
210
+ * @param {Object} [options={}] - Filter options
211
+ * @param {number} [options.priority=0] - Filter priority (higher executes first)
212
+ * @param {string} [options.name] - Filter name for debugging
213
+ * @returns {void}
214
+ * @example
215
+ * this.afterAction('index', function(request) {
216
+ * logger.info({ action: 'index', userId: this.currentUser.id });
217
+ * });
218
+ * @example
219
+ * // With priority
220
+ * this.afterAction(['create', 'update'], function(request) {
221
+ * this.clearCache();
222
+ * }, { priority: 5, name: 'clearCache' });
223
+ */
224
+ afterAction(actionlist, func, options = {}){
225
+ if (!this._validateFilterParams(actionlist, func, 'after')) {
43
226
  return;
44
227
  }
45
228
 
46
- // FIXED: Push to array, don't overwrite
229
+ // FIXED: Push to array with metadata and priority
47
230
  this._afterActionFilters.push({
48
231
  namespace: this.__namespace,
49
232
  actionList: Array.isArray(actionlist) ? actionlist : [actionlist],
50
233
  callBack: func,
51
- that: this
234
+ that: this,
235
+ priority: options.priority || 0,
236
+ name: options.name || func.name || 'anonymous',
237
+ enabled: true,
238
+ registeredAt: new Date().toISOString()
52
239
  });
240
+
241
+ // Sort filters by priority (higher priority first)
242
+ this._afterActionFilters.sort((a, b) => b.priority - a.priority);
53
243
  }
54
244
 
55
- // Check if controller has before action filters for this action
245
+ /**
246
+ * Check if controller has before action filters for this action
247
+ * @private
248
+ * @param {Object} obj - Controller instance
249
+ * @param {Object} request - Request object with toAction property
250
+ * @returns {boolean} True if matching filters exist
251
+ */
56
252
  __hasBeforeAction(obj, request){
57
- if (!this._beforeActionFilters || this._beforeActionFilters.length === 0) {
58
- return false;
59
- }
60
-
61
- return this._beforeActionFilters.some(filter => {
62
- if (filter.namespace !== obj.__namespace) {
63
- return false;
64
- }
65
-
66
- const requestAction = request.toAction.replace(/\s/g, '');
67
- return filter.actionList.some(action => {
68
- const filterAction = action.replace(/\s/g, '');
69
- return filterAction === requestAction;
70
- });
71
- });
253
+ const matchingFilters = this._findMatchingFilters(this._beforeActionFilters, obj, request);
254
+ return matchingFilters.length > 0;
72
255
  }
73
256
 
74
- // Execute all matching before action filters
75
- // FIXED: Async support, error handling, timeout protection, no variable shadowing
257
+ /**
258
+ * Execute all matching before action filters
259
+ * @private
260
+ * @async
261
+ * @param {Object} obj - Controller instance
262
+ * @param {Object} request - Request object
263
+ * @param {EventEmitter} emitter - Event emitter for next() calls
264
+ * @returns {Promise<void>}
265
+ * @throws {Error} If any filter fails
266
+ */
76
267
  async __callBeforeAction(obj, request, emitter) {
77
- if (!this._beforeActionFilters || this._beforeActionFilters.length === 0) {
268
+ // Find all matching filters using optimized helper
269
+ const matchingFilters = this._findMatchingFilters(this._beforeActionFilters, obj, request);
270
+
271
+ if (matchingFilters.length === 0) {
78
272
  return;
79
273
  }
80
274
 
81
- // Find all matching filters
82
- const requestAction = request.toAction.replace(/\s/g, '');
83
- const matchingFilters = this._beforeActionFilters.filter(filter => {
84
- if (filter.namespace !== obj.__namespace) {
85
- return false;
86
- }
87
-
88
- return filter.actionList.some(actionName => {
89
- const normalizedAction = actionName.replace(/\s/g, '');
90
- return normalizedAction === requestAction;
91
- });
92
- });
275
+ const requestAction = this._normalizeAction(request.toAction);
93
276
 
94
277
  // Execute all matching filters in order
95
278
  for (const filter of matchingFilters) {
279
+ const startTime = Date.now();
96
280
  try {
97
281
  // FIXED: Add timeout protection (5 seconds default)
98
282
  await this._executeWithTimeout(
@@ -100,28 +284,36 @@ class MasterActionFilters {
100
284
  filter.that,
101
285
  [request],
102
286
  emitter,
103
- 5000
287
+ this._filterTimeout
104
288
  );
289
+
290
+ // Record successful filter execution timing
291
+ const duration = Date.now() - startTime;
292
+ logger.info({
293
+ code: 'MC_FILTER_EXECUTED',
294
+ message: 'Before action filter executed successfully',
295
+ namespace: filter.namespace,
296
+ action: requestAction,
297
+ duration,
298
+ filterType: 'before'
299
+ });
105
300
  } catch (error) {
106
- // FIXED: Proper error handling
301
+ const duration = Date.now() - startTime;
302
+
303
+ // FIXED: Proper error handling with metrics
107
304
  logger.error({
108
305
  code: 'MC_FILTER_ERROR',
109
306
  message: 'Error in beforeAction filter',
110
307
  namespace: filter.namespace,
111
308
  action: requestAction,
309
+ duration,
310
+ filterType: 'before',
112
311
  error: error.message,
113
312
  stack: error.stack
114
313
  });
115
314
 
116
- // Send error response
117
- const res = request.response;
118
- if (res && !res._headerSent && !res.headersSent) {
119
- res.writeHead(500, { 'Content-Type': 'application/json' });
120
- res.end(JSON.stringify({
121
- error: 'Internal Server Error',
122
- message: MasterActionFilters._master.environmentType === 'development' ? error.message : 'Filter execution failed'
123
- }));
124
- }
315
+ // Send standardized error response (no sensitive info leaked)
316
+ this._sendFilterErrorResponse(request, HTTP_STATUS.INTERNAL_ERROR, 'FILTER_ERROR');
125
317
 
126
318
  // Don't continue to other filters if one fails
127
319
  throw error;
@@ -129,28 +321,27 @@ class MasterActionFilters {
129
321
  }
130
322
  }
131
323
 
132
- // Execute all matching after action filters
133
- // FIXED: Async support, error handling, no variable shadowing
324
+ /**
325
+ * Execute all matching after action filters
326
+ * @private
327
+ * @async
328
+ * @param {Object} obj - Controller instance
329
+ * @param {Object} request - Request object
330
+ * @returns {Promise<void>}
331
+ */
134
332
  async __callAfterAction(obj, request) {
135
- if (!this._afterActionFilters || this._afterActionFilters.length === 0) {
333
+ // Find all matching filters using optimized helper
334
+ const matchingFilters = this._findMatchingFilters(this._afterActionFilters, obj, request);
335
+
336
+ if (matchingFilters.length === 0) {
136
337
  return;
137
338
  }
138
339
 
139
- // Find all matching filters
140
- const requestAction = request.toAction.replace(/\s/g, '');
141
- const matchingFilters = this._afterActionFilters.filter(filter => {
142
- if (filter.namespace !== obj.__namespace) {
143
- return false;
144
- }
145
-
146
- return filter.actionList.some(actionName => {
147
- const normalizedAction = actionName.replace(/\s/g, '');
148
- return normalizedAction === requestAction;
149
- });
150
- });
340
+ const requestAction = this._normalizeAction(request.toAction);
151
341
 
152
342
  // Execute all matching filters in order
153
343
  for (const filter of matchingFilters) {
344
+ const startTime = Date.now();
154
345
  try {
155
346
  // FIXED: Add timeout protection (5 seconds default)
156
347
  await this._executeWithTimeout(
@@ -158,15 +349,30 @@ class MasterActionFilters {
158
349
  filter.that,
159
350
  [request],
160
351
  null,
161
- 5000
352
+ this._filterTimeout
162
353
  );
354
+
355
+ // Record successful filter execution timing
356
+ const duration = Date.now() - startTime;
357
+ logger.info({
358
+ code: 'MC_FILTER_EXECUTED',
359
+ message: 'After action filter executed successfully',
360
+ namespace: filter.namespace,
361
+ action: requestAction,
362
+ duration,
363
+ filterType: 'after'
364
+ });
163
365
  } catch (error) {
164
- // FIXED: Proper error handling
366
+ const duration = Date.now() - startTime;
367
+
368
+ // FIXED: Proper error handling with metrics
165
369
  logger.error({
166
370
  code: 'MC_FILTER_ERROR',
167
371
  message: 'Error in afterAction filter',
168
372
  namespace: filter.namespace,
169
373
  action: requestAction,
374
+ duration,
375
+ filterType: 'after',
170
376
  error: error.message,
171
377
  stack: error.stack
172
378
  });
@@ -176,24 +382,292 @@ class MasterActionFilters {
176
382
  }
177
383
  }
178
384
 
179
- // FIXED: Execute function with timeout protection
385
+ /**
386
+ * Execute filter function with timeout protection
387
+ * @private
388
+ * @async
389
+ * @param {Function} func - Filter callback to execute
390
+ * @param {Object} context - Execution context (controller instance)
391
+ * @param {Array} args - Arguments to pass to filter
392
+ * @param {EventEmitter|null} emitter - Event emitter for next() calls
393
+ * @param {number} timeout - Timeout in milliseconds
394
+ * @returns {Promise<*>} Filter result
395
+ * @throws {Error} If filter times out or throws error
396
+ */
180
397
  async _executeWithTimeout(func, context, args, emitter, timeout) {
398
+ // Validate context exists
399
+ if (!context) {
400
+ throw new Error('Filter execution context is null or undefined');
401
+ }
402
+
403
+ // Validate request in args still has valid response
404
+ if (args[0] && args[0].response) {
405
+ const res = args[0].response;
406
+ if (res._headerSent || res.headersSent) {
407
+ logger.warn({
408
+ code: 'MC_FILTER_WARN_HEADERS_SENT',
409
+ message: 'Filter executing after headers already sent',
410
+ namespace: context.__namespace
411
+ });
412
+ }
413
+ }
414
+
181
415
  // Store emitter in context for next() call
182
416
  if (emitter) {
183
417
  context.__filterEmitter = emitter;
184
418
  }
185
419
 
186
- return Promise.race([
187
- // Execute the filter
188
- Promise.resolve(func.call(context, ...args)),
189
- // Timeout promise
190
- new Promise((_, reject) =>
191
- setTimeout(() => reject(new Error(`Filter timeout after ${timeout}ms`)), timeout)
192
- )
193
- ]);
420
+ // Wrap execution in try-catch for synchronous errors
421
+ try {
422
+ return await Promise.race([
423
+ // Execute the filter
424
+ Promise.resolve(func.call(context, ...args)),
425
+ // Timeout promise
426
+ new Promise((_, reject) =>
427
+ setTimeout(() => reject(new Error(`Filter timeout after ${timeout}ms`)), timeout)
428
+ )
429
+ ]);
430
+ } catch (error) {
431
+ // Ensure error has stack trace
432
+ if (!error.stack) {
433
+ error.stack = new Error().stack;
434
+ }
435
+ throw error;
436
+ }
437
+ }
438
+
439
+ // ==================== Filter Management Utilities ====================
440
+
441
+ /**
442
+ * Remove a before action filter
443
+ * @param {string|Array<string>} actionlist - Action name(s) to remove filter from
444
+ * @param {Function} func - Filter callback function to remove
445
+ * @returns {boolean} True if filter was removed
446
+ * @example
447
+ * this.removeBeforeAction('index', myFilterFunction);
448
+ */
449
+ removeBeforeAction(actionlist, func) {
450
+ const actions = Array.isArray(actionlist) ? actionlist : [actionlist];
451
+ const initialLength = this._beforeActionFilters.length;
452
+
453
+ this._beforeActionFilters = this._beforeActionFilters.filter(filter => {
454
+ // Keep filter if callback doesn't match
455
+ if (filter.callBack !== func) {
456
+ return true;
457
+ }
458
+
459
+ // Keep filter if no actions match
460
+ return !filter.actionList.some(action => actions.includes(action));
461
+ });
462
+
463
+ return this._beforeActionFilters.length < initialLength;
464
+ }
465
+
466
+ /**
467
+ * Remove an after action filter
468
+ * @param {string|Array<string>} actionlist - Action name(s) to remove filter from
469
+ * @param {Function} func - Filter callback function to remove
470
+ * @returns {boolean} True if filter was removed
471
+ * @example
472
+ * this.removeAfterAction('index', myFilterFunction);
473
+ */
474
+ removeAfterAction(actionlist, func) {
475
+ const actions = Array.isArray(actionlist) ? actionlist : [actionlist];
476
+ const initialLength = this._afterActionFilters.length;
477
+
478
+ this._afterActionFilters = this._afterActionFilters.filter(filter => {
479
+ // Keep filter if callback doesn't match
480
+ if (filter.callBack !== func) {
481
+ return true;
482
+ }
483
+
484
+ // Keep filter if no actions match
485
+ return !filter.actionList.some(action => actions.includes(action));
486
+ });
487
+
488
+ return this._afterActionFilters.length < initialLength;
489
+ }
490
+
491
+ /**
492
+ * Clear all filters (useful for testing)
493
+ * @param {string} [type] - 'before', 'after', or undefined for all
494
+ * @returns {void}
495
+ * @example
496
+ * this.clearFilters(); // Clear all
497
+ * this.clearFilters('before'); // Clear only before filters
498
+ */
499
+ clearFilters(type) {
500
+ if (!type || type === 'before') {
501
+ this._beforeActionFilters = [];
502
+ }
503
+ if (!type || type === 'after') {
504
+ this._afterActionFilters = [];
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Get all registered filters for debugging
510
+ * @param {string} [type] - 'before', 'after', or undefined for all
511
+ * @returns {Object} Filter information
512
+ * @example
513
+ * const filters = this.getRegisteredFilters();
514
+ * console.log(filters.before); // Array of before filters
515
+ */
516
+ getRegisteredFilters(type) {
517
+ const result = {};
518
+
519
+ if (!type || type === 'before') {
520
+ result.before = this._beforeActionFilters.map(f => ({
521
+ namespace: f.namespace,
522
+ actions: f.actionList,
523
+ name: f.name,
524
+ priority: f.priority,
525
+ enabled: f.enabled,
526
+ registeredAt: f.registeredAt
527
+ }));
528
+ }
529
+
530
+ if (!type || type === 'after') {
531
+ result.after = this._afterActionFilters.map(f => ({
532
+ namespace: f.namespace,
533
+ actions: f.actionList,
534
+ name: f.name,
535
+ priority: f.priority,
536
+ enabled: f.enabled,
537
+ registeredAt: f.registeredAt
538
+ }));
539
+ }
540
+
541
+ return result;
542
+ }
543
+
544
+ /**
545
+ * Check if a filter is registered
546
+ * @param {string} type - 'before' or 'after'
547
+ * @param {string} action - Action name
548
+ * @param {string} [filterName] - Optional filter name to check
549
+ * @returns {boolean} True if filter exists
550
+ * @example
551
+ * if (this.hasFilter('before', 'index', 'authCheck')) { ... }
552
+ */
553
+ hasFilter(type, action, filterName) {
554
+ const filters = type === 'before' ? this._beforeActionFilters : this._afterActionFilters;
555
+ const normalizedAction = this._normalizeAction(action);
556
+
557
+ return filters.some(filter => {
558
+ if (filterName && filter.name !== filterName) {
559
+ return false;
560
+ }
561
+
562
+ return filter.actionList.some(a =>
563
+ this._normalizeAction(a) === normalizedAction
564
+ );
565
+ });
566
+ }
567
+
568
+ /**
569
+ * Enable or disable a filter by name
570
+ * @param {string} type - 'before' or 'after'
571
+ * @param {string} filterName - Filter name
572
+ * @param {boolean} enabled - True to enable, false to disable
573
+ * @returns {boolean} True if filter was found and updated
574
+ * @example
575
+ * this.setFilterEnabled('before', 'authCheck', false); // Disable filter
576
+ */
577
+ setFilterEnabled(type, filterName, enabled) {
578
+ const filters = type === 'before' ? this._beforeActionFilters : this._afterActionFilters;
579
+ let found = false;
580
+
581
+ filters.forEach(filter => {
582
+ if (filter.name === filterName) {
583
+ filter.enabled = enabled;
584
+ found = true;
585
+ }
586
+ });
587
+
588
+ return found;
589
+ }
590
+
591
+ // ==================== Test Utilities ====================
592
+
593
+ /**
594
+ * Get filter count for testing
595
+ * @private
596
+ * @param {string} [type] - 'before', 'after', or undefined for total
597
+ * @returns {number} Number of registered filters
598
+ */
599
+ _getFilterCount(type) {
600
+ if (type === 'before') {
601
+ return this._beforeActionFilters.length;
602
+ }
603
+ if (type === 'after') {
604
+ return this._afterActionFilters.length;
605
+ }
606
+ return this._beforeActionFilters.length + this._afterActionFilters.length;
607
+ }
608
+
609
+ /**
610
+ * Reset all filters for test isolation
611
+ * @private
612
+ * @returns {void}
613
+ */
614
+ _resetFilters() {
615
+ this._beforeActionFilters = [];
616
+ this._afterActionFilters = [];
617
+ }
618
+
619
+ /**
620
+ * Check if a specific filter is registered by callback reference
621
+ * @private
622
+ * @param {string} type - 'before' or 'after'
623
+ * @param {Function} callback - Filter callback to check
624
+ * @returns {boolean} True if filter is registered
625
+ */
626
+ _isFilterRegistered(type, callback) {
627
+ const filters = type === 'before' ? this._beforeActionFilters : this._afterActionFilters;
628
+ return filters.some(filter => filter.callBack === callback);
629
+ }
630
+
631
+ /**
632
+ * Get filter timeout value for testing
633
+ * @private
634
+ * @returns {number} Timeout in milliseconds
635
+ */
636
+ _getFilterTimeout() {
637
+ return this._filterTimeout;
638
+ }
639
+
640
+ /**
641
+ * Set filter timeout value (useful for testing)
642
+ * @private
643
+ * @param {number} timeout - Timeout in milliseconds
644
+ * @returns {void}
645
+ */
646
+ _setFilterTimeout(timeout) {
647
+ if (typeof timeout === 'number' && timeout > 0) {
648
+ this._filterTimeout = timeout;
649
+ } else {
650
+ logger.warn({
651
+ code: 'MC_FILTER_INVALID_TIMEOUT',
652
+ message: 'Invalid timeout value, must be positive number',
653
+ timeout
654
+ });
655
+ }
194
656
  }
195
657
 
196
- // FIXED: Request-scoped next() function
658
+ /**
659
+ * Continue to the next filter or action
660
+ * Call this from beforeAction filters to proceed with request execution
661
+ * @returns {void}
662
+ * @example
663
+ * this.beforeAction('index', function() {
664
+ * if (this.isAuthenticated()) {
665
+ * this.next(); // Continue
666
+ * } else {
667
+ * this.redirectTo('/login'); // Stop and redirect
668
+ * }
669
+ * });
670
+ */
197
671
  next(){
198
672
  if (this.__filterEmitter) {
199
673
  this.__filterEmitter.emit("controller");