mastercontroller 1.3.36 → 1.3.37

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.
@@ -32,12 +32,14 @@ class MasterErrorLogger {
32
32
  file: options.file || null,
33
33
  sampleRate: options.sampleRate || 1.0, // Log 100% by default
34
34
  maxFileSize: options.maxFileSize || 10 * 1024 * 1024, // 10MB
35
+ dedupeWindowMs: options.dedupeWindowMs || 5000, // Suppress duplicate errors within 5s
35
36
  ...options
36
37
  };
37
38
 
38
39
  this.backends = [];
39
40
  this.errorCount = 0;
40
41
  this.sessionId = this._generateSessionId();
42
+ this._recentErrors = new Map(); // code -> { count, firstSeen, lastSeen }
41
43
 
42
44
  // Setup default backends
43
45
  if (this.options.console) {
@@ -76,18 +78,47 @@ class MasterErrorLogger {
76
78
  return;
77
79
  }
78
80
 
81
+ // Deduplicate repeated errors within the window
82
+ const code = data.code || 'UNKNOWN';
83
+ if (level >= LOG_LEVELS.ERROR) {
84
+ const now = Date.now();
85
+ const recent = this._recentErrors.get(code);
86
+ if (recent && (now - recent.firstSeen) < this.options.dedupeWindowMs) {
87
+ recent.count++;
88
+ recent.lastSeen = now;
89
+ return; // Suppress duplicate
90
+ }
91
+ // Flush summary of previous burst if there was one
92
+ if (recent && recent.count > 1) {
93
+ const summary = this._formatLogEntry({
94
+ code: code,
95
+ message: `Suppressed ${recent.count - 1} duplicate entries of ${code} (${recent.lastSeen - recent.firstSeen}ms window)`,
96
+ level: LOG_LEVELS.WARN
97
+ }, LOG_LEVELS.WARN);
98
+ this._dispatch(summary);
99
+ }
100
+ this._recentErrors.set(code, { count: 1, firstSeen: now, lastSeen: now });
101
+ }
102
+
79
103
  const entry = this._formatLogEntry(data, level);
104
+ this._dispatch(entry);
105
+ this.errorCount++;
106
+ }
80
107
 
81
- // Send to all backends
108
+ /**
109
+ * Dispatch entry to all backends
110
+ */
111
+ _dispatch(entry) {
82
112
  this.backends.forEach(backend => {
83
113
  try {
84
114
  backend(entry);
85
115
  } catch (error) {
86
- console.error('[MasterErrorLogger] Backend failed:', error.message);
116
+ // Avoid console methods that can trigger EPIPE recursion
117
+ if (error.code !== 'EPIPE' && error.code !== 'ERR_STREAM_DESTROYED') {
118
+ try { process.stderr.write(`[MasterErrorLogger] Backend failed: ${error.message}\n`); } catch (_) {}
119
+ }
87
120
  }
88
121
  });
89
-
90
- this.errorCount++;
91
122
  }
92
123
 
93
124
  /**
@@ -117,28 +148,39 @@ class MasterErrorLogger {
117
148
  * Format log entry with metadata
118
149
  */
119
150
  _formatLogEntry(data, level) {
120
- return {
151
+ const entry = {
121
152
  timestamp: new Date().toISOString(),
122
153
  sessionId: this.sessionId,
123
154
  level: LOG_LEVEL_NAMES[level],
124
155
  code: data.code || 'UNKNOWN',
125
- message: data.message || 'No message provided',
126
- component: data.component || null,
127
- file: data.file || null,
128
- line: data.line || null,
129
- route: data.route || null,
130
- context: data.context || {},
131
- stack: data.stack || null,
132
- originalError: data.originalError ? {
133
- message: data.originalError.message,
134
- stack: data.originalError.stack
135
- } : null,
136
- environment: process.env.NODE_ENV || 'development',
137
- nodeVersion: process.version,
138
- platform: process.platform,
139
- memory: process.memoryUsage(),
140
- uptime: process.uptime()
156
+ message: data.message || 'No message provided'
141
157
  };
158
+
159
+ // Only include optional fields when they have values
160
+ if (data.component) entry.component = data.component;
161
+ if (data.file) entry.file = data.file;
162
+ if (data.line) entry.line = data.line;
163
+ if (data.route) entry.route = data.route;
164
+ if (data.context && Object.keys(data.context).length > 0) entry.context = data.context;
165
+
166
+ // Include stack once — prefer originalError.stack to avoid duplication
167
+ if (data.originalError) {
168
+ entry.stack = data.originalError.stack || data.stack || null;
169
+ } else if (data.stack) {
170
+ entry.stack = data.stack;
171
+ }
172
+
173
+ entry.environment = process.env.NODE_ENV || 'development';
174
+
175
+ // Only include memory/system info on ERROR and FATAL
176
+ if (level >= LOG_LEVELS.ERROR) {
177
+ entry.memory = process.memoryUsage();
178
+ entry.nodeVersion = process.version;
179
+ entry.platform = process.platform;
180
+ entry.uptime = process.uptime();
181
+ }
182
+
183
+ return entry;
142
184
  }
143
185
 
144
186
  /**
@@ -156,24 +198,26 @@ class MasterErrorLogger {
156
198
  const color = levelColors[entry.level] || '';
157
199
  const reset = '\x1b[0m';
158
200
 
159
- const logFn = entry.level === 'DEBUG' || entry.level === 'INFO' ? console.log :
160
- entry.level === 'WARN' ? console.warn : console.error;
201
+ // Use process.stdout/stderr.write directly to avoid EPIPE recursion
202
+ // through console.log -> broken pipe -> uncaughtException -> logger -> console.log
203
+ const stream = (entry.level === 'DEBUG' || entry.level === 'INFO') ? process.stdout : process.stderr;
161
204
 
162
- logFn(
163
- `${color}[${entry.timestamp}] [${entry.level}]${reset} ${entry.code}:`,
164
- entry.message
165
- );
205
+ try {
206
+ stream.write(`${color}[${entry.timestamp}] [${entry.level}]${reset} ${entry.code}: ${entry.message}\n`);
166
207
 
167
- if (entry.component) {
168
- logFn(` Component: ${entry.component}`);
169
- }
208
+ if (entry.component) {
209
+ stream.write(` Component: ${entry.component}\n`);
210
+ }
170
211
 
171
- if (entry.file) {
172
- logFn(` File: ${entry.file}${entry.line ? `:${entry.line}` : ''}`);
173
- }
212
+ if (entry.file) {
213
+ stream.write(` File: ${entry.file}${entry.line ? `:${entry.line}` : ''}\n`);
214
+ }
174
215
 
175
- if (entry.stack && process.env.NODE_ENV !== 'production') {
176
- logFn(` Stack: ${entry.stack}`);
216
+ if (entry.stack && process.env.NODE_ENV !== 'production') {
217
+ stream.write(` Stack: ${entry.stack}\n`);
218
+ }
219
+ } catch (err) {
220
+ // If the stream is broken (EPIPE), silently drop — do not recurse
177
221
  }
178
222
  }
179
223
 
@@ -172,7 +172,14 @@ function extractUserCodeContext(stack) {
172
172
  function setupGlobalErrorHandlers() {
173
173
  // Handle uncaught exceptions
174
174
  process.on('uncaughtException', (error) => {
175
- console.error('[MasterController] Uncaught Exception:', error);
175
+ // EPIPE/stream errors from logging itself — do not recurse
176
+ if (error.code === 'EPIPE' || error.code === 'ERR_STREAM_DESTROYED') {
177
+ try { process.stderr.write(`[MasterController] Stream error suppressed: ${error.code}\n`); } catch (_) {}
178
+ return;
179
+ }
180
+
181
+ // Use stderr.write instead of console.error to avoid EPIPE recursion
182
+ try { process.stderr.write(`[MasterController] Uncaught Exception: ${error.message}\n`); } catch (_) {}
176
183
 
177
184
  // Extract context from stack trace
178
185
  const context = extractUserCodeContext(error.stack);
@@ -181,34 +188,33 @@ function setupGlobalErrorHandlers() {
181
188
  let enhancedMessage = `Uncaught exception: ${error.message}`;
182
189
 
183
190
  if (context && context.triggeringFile) {
184
- enhancedMessage += `\n\nšŸ” Error Location: ${context.triggeringFile.location}`;
191
+ enhancedMessage += `\n\nError Location: ${context.triggeringFile.location}`;
185
192
  }
186
193
 
187
194
  if (context && context.userFiles.length > 0) {
188
- enhancedMessage += `\n\nšŸ“‚ Your Code Involved:`;
195
+ enhancedMessage += `\n\nYour Code Involved:`;
189
196
  context.userFiles.forEach((file, i) => {
190
- if (i < 3) { // Show first 3 user files
197
+ if (i < 3) {
191
198
  enhancedMessage += `\n ${i + 1}. ${file.location}`;
192
199
  }
193
200
  });
194
201
  }
195
202
 
196
203
  if (context && context.frameworkFiles.length > 0) {
197
- enhancedMessage += `\n\nšŸ”§ Framework Files Involved:`;
204
+ enhancedMessage += `\n\nFramework Files Involved:`;
198
205
  context.frameworkFiles.forEach((file, i) => {
199
- if (i < 2) { // Show first 2 framework files
206
+ if (i < 2) {
200
207
  enhancedMessage += `\n ${i + 1}. ${file.location}`;
201
208
  }
202
209
  });
203
210
  }
204
211
 
205
- console.error(enhancedMessage);
212
+ try { process.stderr.write(enhancedMessage + '\n'); } catch (_) {}
206
213
 
207
214
  logger.fatal({
208
215
  code: 'MC_ERR_UNCAUGHT_EXCEPTION',
209
216
  message: enhancedMessage,
210
217
  originalError: error,
211
- stack: error.stack,
212
218
  context: context
213
219
  });
214
220
 
@@ -220,7 +226,7 @@ function setupGlobalErrorHandlers() {
220
226
 
221
227
  // Handle unhandled promise rejections
222
228
  process.on('unhandledRejection', (reason, promise) => {
223
- console.error('[MasterController] Unhandled Rejection:', reason);
229
+ try { process.stderr.write(`[MasterController] Unhandled Rejection: ${reason}\n`); } catch (_) {}
224
230
 
225
231
  // Extract context from stack trace if available
226
232
  const context = reason?.stack ? extractUserCodeContext(reason.stack) : null;
@@ -229,27 +235,26 @@ function setupGlobalErrorHandlers() {
229
235
  let enhancedMessage = `Unhandled promise rejection: ${reason}`;
230
236
 
231
237
  if (context && context.triggeringFile) {
232
- enhancedMessage += `\n\nšŸ” Error Location: ${context.triggeringFile.location}`;
238
+ enhancedMessage += `\n\nError Location: ${context.triggeringFile.location}`;
233
239
  }
234
240
 
235
241
  if (context && context.userFiles.length > 0) {
236
- enhancedMessage += `\n\nšŸ“‚ Your Code Involved:`;
242
+ enhancedMessage += `\n\nYour Code Involved:`;
237
243
  context.userFiles.forEach((file, i) => {
238
- if (i < 3) { // Show first 3 user files
244
+ if (i < 3) {
239
245
  enhancedMessage += `\n ${i + 1}. ${file.location}`;
240
246
  }
241
247
  });
242
248
  }
243
249
 
244
250
  if (enhancedMessage !== `Unhandled promise rejection: ${reason}`) {
245
- console.error(enhancedMessage);
251
+ try { process.stderr.write(enhancedMessage + '\n'); } catch (_) {}
246
252
  }
247
253
 
248
254
  logger.error({
249
255
  code: 'MC_ERR_UNHANDLED_REJECTION',
250
256
  message: enhancedMessage,
251
257
  originalError: reason,
252
- stack: reason?.stack,
253
258
  context: context
254
259
  });
255
260
  });
@@ -257,7 +262,7 @@ function setupGlobalErrorHandlers() {
257
262
  // Handle warnings
258
263
  process.on('warning', (warning) => {
259
264
  if (isDevelopment) {
260
- console.warn('[MasterController] Warning:', warning);
265
+ try { process.stderr.write(`[MasterController] Warning: ${warning.message}\n`); } catch (_) {}
261
266
  }
262
267
 
263
268
  logger.warn({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mastercontroller",
3
- "version": "1.3.36",
3
+ "version": "1.3.37",
4
4
  "description": "Fortune 500 ready Node.js MVC framework with enterprise security, monitoring, and horizontal scaling",
5
5
  "main": "MasterControl.js",
6
6
  "license": "MIT",
@@ -36,18 +36,25 @@ class SessionSecurity {
36
36
  * Session middleware
37
37
  */
38
38
  middleware() {
39
- return async (req, res, next) => {
39
+ const self = this;
40
+
41
+ // Return pipeline-compatible (ctx, next) middleware.
42
+ // MasterPipeline.execute() calls handler(ctx, next), not handler(req, res, next).
43
+ return async (ctx, next) => {
44
+ const req = ctx.request;
45
+ const res = ctx.response;
46
+
40
47
  // Parse session from cookie
41
- const sessionId = this._parseCookie(req);
48
+ const sessionId = self._parseCookie(req);
42
49
 
43
50
  if (sessionId) {
44
51
  // Load existing session
45
52
  const session = sessionStore.get(sessionId);
46
53
 
47
- if (session && this._isSessionValid(session, req)) {
54
+ if (session && self._isSessionValid(session, req)) {
48
55
  // Check if session needs regeneration
49
- if (this._shouldRegenerate(session)) {
50
- req.session = await this._regenerateSession(sessionId, session, res);
56
+ if (self._shouldRegenerate(session)) {
57
+ req.session = await self._regenerateSession(sessionId, session, res);
51
58
  } else {
52
59
  // Use existing session
53
60
  req.session = session.data;
@@ -57,9 +64,9 @@ class SessionSecurity {
57
64
  session.lastAccess = Date.now();
58
65
 
59
66
  // Extend expiry if rolling
60
- if (this.rolling) {
61
- session.expiry = Date.now() + this.maxAge;
62
- this._setCookie(res, sessionId);
67
+ if (self.rolling) {
68
+ session.expiry = Date.now() + self.maxAge;
69
+ self._setCookie(res, sessionId);
63
70
  }
64
71
  }
65
72
  } else {
@@ -74,24 +81,24 @@ class SessionSecurity {
74
81
  }
75
82
 
76
83
  // Create new session
77
- req.session = await this._createSession(req, res);
84
+ req.session = await self._createSession(req, res);
78
85
  }
79
86
  } else {
80
87
  // No session cookie, create new session
81
- req.session = await this._createSession(req, res);
88
+ req.session = await self._createSession(req, res);
82
89
  }
83
90
 
84
91
  // Save session on response
85
92
  if (typeof res?.end === 'function') {
86
93
  const originalEnd = res.end;
87
94
  res.end = (...args) => {
88
- this._saveSession(req);
95
+ self._saveSession(req);
89
96
  originalEnd.apply(res, args);
90
97
  };
91
98
  }
92
99
 
93
100
  if (typeof next === 'function') {
94
- next();
101
+ await next();
95
102
  }
96
103
  };
97
104
  }