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/MasterAction.js +302 -62
- package/MasterActionFilters.js +556 -82
- package/MasterControl.js +77 -44
- package/MasterCors.js +61 -19
- package/MasterPipeline.js +29 -6
- package/MasterRequest.js +579 -102
- package/MasterRouter.js +446 -75
- package/MasterSocket.js +380 -15
- package/MasterTemp.js +292 -10
- package/MasterTimeout.js +420 -64
- package/MasterTools.js +478 -77
- package/README.md +505 -0
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -29
- package/.github/workflows/ci.yml +0 -317
- package/PERFORMANCE_SECURITY_AUDIT.md +0 -677
- package/SENIOR_ENGINEER_AUDIT.md +0 -2477
- package/VERIFICATION_CHECKLIST.md +0 -726
- package/log/mastercontroller.log +0 -2
- package/test-json-empty-body.js +0 -76
- package/test-raw-body-preservation.js +0 -128
- package/test-v1.3.4-fixes.js +0 -129
package/MasterSocket.js
CHANGED
|
@@ -3,11 +3,118 @@
|
|
|
3
3
|
const { Server } = require('socket.io');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { logger } = require('./error/MasterErrorLogger');
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// Socket Configuration Constants
|
|
9
|
+
const SOCKET_CONFIG = {
|
|
10
|
+
DEFAULT_TIMEOUT: 30000, // 30 seconds
|
|
11
|
+
MAX_EVENT_NAME_LENGTH: 255,
|
|
12
|
+
MAX_PAYLOAD_SIZE: 10 * 1024 * 1024 // 10MB
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Socket Event Names
|
|
16
|
+
const SOCKET_EVENTS = {
|
|
17
|
+
CONNECTION: 'connection',
|
|
18
|
+
DISCONNECT: 'disconnect',
|
|
19
|
+
ERROR: 'error',
|
|
20
|
+
CONNECT_ERROR: 'connect_error'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Transport Types
|
|
24
|
+
const TRANSPORT_TYPES = {
|
|
25
|
+
WEBSOCKET: 'websocket',
|
|
26
|
+
POLLING: 'polling'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// HTTP Methods for CORS
|
|
30
|
+
const HTTP_METHODS = {
|
|
31
|
+
GET: 'GET',
|
|
32
|
+
POST: 'POST',
|
|
33
|
+
PUT: 'PUT',
|
|
34
|
+
DELETE: 'DELETE'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate controller/action name for security
|
|
39
|
+
*
|
|
40
|
+
* @param {string} name - Name to validate
|
|
41
|
+
* @param {string} type - Type (controller or action)
|
|
42
|
+
* @throws {Error} If name is invalid
|
|
43
|
+
*/
|
|
44
|
+
function validateSocketIdentifier(name, type) {
|
|
45
|
+
if (!name || typeof name !== 'string') {
|
|
46
|
+
throw new TypeError(`${type} name must be a non-empty string`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Must be valid JavaScript identifier (no path traversal, no special chars)
|
|
50
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
51
|
+
throw new Error(`Invalid ${type} name: ${name}. Must be a valid identifier.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for path traversal attempts
|
|
55
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
56
|
+
throw new Error(`Security violation: ${type} name contains path traversal characters`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check max length
|
|
60
|
+
if (name.length > SOCKET_CONFIG.MAX_EVENT_NAME_LENGTH) {
|
|
61
|
+
throw new Error(`${type} name exceeds maximum length (${SOCKET_CONFIG.MAX_EVENT_NAME_LENGTH})`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate socket data array
|
|
67
|
+
*
|
|
68
|
+
* @param {Array} data - Data array to validate
|
|
69
|
+
* @throws {Error} If data is invalid
|
|
70
|
+
*/
|
|
71
|
+
function validateSocketData(data) {
|
|
72
|
+
if (!Array.isArray(data)) {
|
|
73
|
+
throw new TypeError('Socket data must be an array');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (data.length < 2) {
|
|
77
|
+
throw new Error('Socket data must contain [action, payload]');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const [action, payload] = data;
|
|
81
|
+
|
|
82
|
+
if (!action || typeof action !== 'string') {
|
|
83
|
+
throw new TypeError('Socket action (data[0]) must be a non-empty string');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
validateSocketIdentifier(action, 'action');
|
|
87
|
+
|
|
88
|
+
// Check payload size (prevent DoS)
|
|
89
|
+
const payloadStr = JSON.stringify(payload || {});
|
|
90
|
+
if (payloadStr.length > SOCKET_CONFIG.MAX_PAYLOAD_SIZE) {
|
|
91
|
+
throw new Error(`Payload exceeds maximum size (${SOCKET_CONFIG.MAX_PAYLOAD_SIZE} bytes)`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Uppercase first character of a string
|
|
97
|
+
*
|
|
98
|
+
* @param {string} string - String to transform
|
|
99
|
+
* @returns {string} String with first character uppercased
|
|
100
|
+
* @example
|
|
101
|
+
* jsUcfirst("hello") // Returns: "Hello"
|
|
102
|
+
*/
|
|
103
|
+
const jsUcfirst = function(string){
|
|
8
104
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
9
105
|
};
|
|
10
106
|
|
|
107
|
+
/**
|
|
108
|
+
* MasterSocket - WebSocket management with Socket.IO
|
|
109
|
+
*
|
|
110
|
+
* Handles:
|
|
111
|
+
* - Socket.IO server initialization with CORS
|
|
112
|
+
* - Event routing to socket controllers
|
|
113
|
+
* - Cross-platform socket module loading
|
|
114
|
+
* - Automatic CORS configuration from config/initializers/cors.json
|
|
115
|
+
*
|
|
116
|
+
* @class MasterSocket
|
|
117
|
+
*/
|
|
11
118
|
class MasterSocket{
|
|
12
119
|
|
|
13
120
|
// Lazy-load master to avoid circular dependency (Google-style lazy initialization)
|
|
@@ -18,7 +125,43 @@ class MasterSocket{
|
|
|
18
125
|
return this.__masterCache;
|
|
19
126
|
}
|
|
20
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Initialize Socket.IO server
|
|
130
|
+
*
|
|
131
|
+
* Supports three initialization patterns:
|
|
132
|
+
* 1. Pass HTTP server: init(server, options)
|
|
133
|
+
* 2. Pass Socket.IO instance: init(io)
|
|
134
|
+
* 3. Use master server: init(undefined, options) or init()
|
|
135
|
+
*
|
|
136
|
+
* Automatically loads CORS config from config/initializers/cors.json
|
|
137
|
+
*
|
|
138
|
+
* @param {Server|Object} [serverOrIo] - HTTP server or Socket.IO instance
|
|
139
|
+
* @param {Object} [options={}] - Socket.IO options to merge with defaults
|
|
140
|
+
* @param {Object} [options.cors] - CORS configuration
|
|
141
|
+
* @param {string|string[]|boolean} [options.cors.origin] - Allowed origins
|
|
142
|
+
* @param {boolean} [options.cors.credentials] - Allow credentials
|
|
143
|
+
* @param {string[]} [options.cors.methods] - Allowed HTTP methods
|
|
144
|
+
* @param {string[]} [options.transports] - Transport types ['websocket', 'polling']
|
|
145
|
+
* @throws {Error} If no HTTP server is available
|
|
146
|
+
* @returns {void}
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // Pattern 1: Pass server explicitly
|
|
150
|
+
* this._master.socket.init(server, { cors: { origin: 'https://app.com' }});
|
|
151
|
+
*
|
|
152
|
+
* // Pattern 2: Pass pre-configured Socket.IO instance
|
|
153
|
+
* const io = new Server(server, opts);
|
|
154
|
+
* this._master.socket.init(io);
|
|
155
|
+
*
|
|
156
|
+
* // Pattern 3: Use master server (call after this._master.start(server))
|
|
157
|
+
* this._master.socket.init();
|
|
158
|
+
*/
|
|
21
159
|
init(serverOrIo, options = {}){
|
|
160
|
+
// Input validation
|
|
161
|
+
if (options !== undefined && (typeof options !== 'object' || options === null || Array.isArray(options))) {
|
|
162
|
+
throw new TypeError('Socket options must be an object');
|
|
163
|
+
}
|
|
164
|
+
|
|
22
165
|
this._baseurl = this._master.root;
|
|
23
166
|
|
|
24
167
|
// Build Socket.IO options using master cors initializer when available
|
|
@@ -46,9 +189,24 @@ class MasterSocket{
|
|
|
46
189
|
this._bind();
|
|
47
190
|
}
|
|
48
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Build default Socket.IO options with CORS configuration
|
|
194
|
+
*
|
|
195
|
+
* Loads CORS settings from config/initializers/cors.json if available.
|
|
196
|
+
* Falls back to sensible defaults for development.
|
|
197
|
+
*
|
|
198
|
+
* @private
|
|
199
|
+
* @returns {Object} Socket.IO options object
|
|
200
|
+
* @returns {Object} result.cors - CORS configuration
|
|
201
|
+
* @returns {string[]} result.transports - Transport types
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* const opts = this._buildDefaultIoOptions();
|
|
205
|
+
* // Returns: { cors: { origin: true, credentials: true, methods: ['GET', 'POST'] }, transports: ['websocket', 'polling'] }
|
|
206
|
+
*/
|
|
49
207
|
_buildDefaultIoOptions(){
|
|
50
208
|
const corsCfg = this._loadCorsConfig();
|
|
51
|
-
const transports = [
|
|
209
|
+
const transports = [TRANSPORT_TYPES.WEBSOCKET, TRANSPORT_TYPES.POLLING];
|
|
52
210
|
const cors = {};
|
|
53
211
|
try {
|
|
54
212
|
if (corsCfg) {
|
|
@@ -60,12 +218,27 @@ class MasterSocket{
|
|
|
60
218
|
// sensible defaults for dev
|
|
61
219
|
cors.origin = true;
|
|
62
220
|
cors.credentials = true;
|
|
63
|
-
cors.methods = [
|
|
221
|
+
cors.methods = [HTTP_METHODS.GET, HTTP_METHODS.POST];
|
|
64
222
|
}
|
|
65
223
|
} catch (_) {}
|
|
66
224
|
return { cors, transports };
|
|
67
225
|
}
|
|
68
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Load CORS configuration from config/initializers/cors.json
|
|
229
|
+
*
|
|
230
|
+
* @private
|
|
231
|
+
* @returns {Object|null} CORS configuration object or null if not found
|
|
232
|
+
* @returns {string|string[]|boolean} result.origin - Allowed origins
|
|
233
|
+
* @returns {boolean} result.credentials - Allow credentials
|
|
234
|
+
* @returns {string[]} result.methods - Allowed HTTP methods
|
|
235
|
+
* @returns {string[]} result.allowedHeaders - Allowed request headers
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* const corsCfg = this._loadCorsConfig();
|
|
239
|
+
* // Returns: { origin: ['https://app.com'], credentials: true, methods: ['GET', 'POST'] }
|
|
240
|
+
* // or null if file not found
|
|
241
|
+
*/
|
|
69
242
|
_loadCorsConfig(){
|
|
70
243
|
try {
|
|
71
244
|
const cfgPath = path.join(this._master.root, 'config', 'initializers', 'cors.json');
|
|
@@ -74,52 +247,183 @@ class MasterSocket{
|
|
|
74
247
|
return JSON.parse(raw);
|
|
75
248
|
}
|
|
76
249
|
} catch (e) {
|
|
77
|
-
|
|
250
|
+
logger.warn({
|
|
251
|
+
code: 'MC_SOCKET_CORS_LOAD_FAILED',
|
|
252
|
+
message: 'Failed to load cors.json configuration',
|
|
253
|
+
error: e.message,
|
|
254
|
+
path: cfgPath
|
|
255
|
+
});
|
|
78
256
|
}
|
|
79
257
|
return null;
|
|
80
258
|
}
|
|
81
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Bind Socket.IO event handlers
|
|
262
|
+
*
|
|
263
|
+
* Sets up connection handler and routes all socket events through
|
|
264
|
+
* the MasterSocket.load() method for controller dispatch.
|
|
265
|
+
*
|
|
266
|
+
* @private
|
|
267
|
+
* @returns {void}
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* // Called internally by init()
|
|
271
|
+
* this._bind();
|
|
272
|
+
*/
|
|
82
273
|
_bind(){
|
|
83
274
|
const io = this.io;
|
|
84
|
-
io.on(
|
|
275
|
+
io.on(SOCKET_EVENTS.CONNECTION, (socket) => {
|
|
85
276
|
try{
|
|
277
|
+
logger.info({
|
|
278
|
+
code: 'MC_SOCKET_CONNECTED',
|
|
279
|
+
message: 'Socket client connected',
|
|
280
|
+
socketId: socket.id,
|
|
281
|
+
controller: socket.handshake?.query?.socket
|
|
282
|
+
});
|
|
283
|
+
|
|
86
284
|
// Route all events through MasterSocket loader
|
|
87
285
|
socket.onAny((eventName, payload) => {
|
|
88
286
|
try{
|
|
89
287
|
// MasterSocket.load expects [action, payload]
|
|
90
288
|
const data = [eventName, payload];
|
|
91
|
-
|
|
289
|
+
// CRITICAL FIX: Use this._master instead of undefined 'master'
|
|
290
|
+
if (this._master && this._master.socket && typeof this._master.socket.load === 'function') {
|
|
92
291
|
this._master.socket.load(data, socket, io);
|
|
93
292
|
}
|
|
94
293
|
}catch(e){
|
|
95
|
-
|
|
294
|
+
logger.error({
|
|
295
|
+
code: 'MC_SOCKET_ROUTING_ERROR',
|
|
296
|
+
message: 'Socket event routing failed',
|
|
297
|
+
socketId: socket.id,
|
|
298
|
+
eventName,
|
|
299
|
+
error: e.message,
|
|
300
|
+
stack: e.stack
|
|
301
|
+
});
|
|
96
302
|
}
|
|
97
303
|
});
|
|
304
|
+
|
|
305
|
+
// MEMORY LEAK FIX: Add disconnect handler for cleanup
|
|
306
|
+
socket.on(SOCKET_EVENTS.DISCONNECT, (reason) => {
|
|
307
|
+
try {
|
|
308
|
+
logger.info({
|
|
309
|
+
code: 'MC_SOCKET_DISCONNECTED',
|
|
310
|
+
message: 'Socket client disconnected',
|
|
311
|
+
socketId: socket.id,
|
|
312
|
+
reason,
|
|
313
|
+
controller: socket.handshake?.query?.socket
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Clean up event listeners
|
|
317
|
+
socket.offAny();
|
|
318
|
+
socket.removeAllListeners();
|
|
319
|
+
} catch (cleanupError) {
|
|
320
|
+
logger.error({
|
|
321
|
+
code: 'MC_SOCKET_CLEANUP_ERROR',
|
|
322
|
+
message: 'Socket cleanup failed',
|
|
323
|
+
socketId: socket.id,
|
|
324
|
+
error: cleanupError.message
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Handle socket errors
|
|
330
|
+
socket.on(SOCKET_EVENTS.ERROR, (error) => {
|
|
331
|
+
logger.error({
|
|
332
|
+
code: 'MC_SOCKET_ERROR',
|
|
333
|
+
message: 'Socket error occurred',
|
|
334
|
+
socketId: socket.id,
|
|
335
|
+
error: error.message || error
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
98
339
|
}catch(e){
|
|
99
|
-
|
|
340
|
+
logger.error({
|
|
341
|
+
code: 'MC_SOCKET_CONNECTION_ERROR',
|
|
342
|
+
message: 'Socket connection handler failed',
|
|
343
|
+
error: e.message,
|
|
344
|
+
stack: e.stack
|
|
345
|
+
});
|
|
100
346
|
}
|
|
101
347
|
});
|
|
102
348
|
}
|
|
103
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Load and execute socket controller action
|
|
352
|
+
*
|
|
353
|
+
* Routes socket events to appropriate controller actions. Supports:
|
|
354
|
+
* - PascalCase and camelCase controller names (cross-platform)
|
|
355
|
+
* - Before action filters via callBeforeAction()
|
|
356
|
+
* - Automatic error handling and logging
|
|
357
|
+
*
|
|
358
|
+
* Controller file location: {root}/app/sockets/{Controller}Socket.js
|
|
359
|
+
* Controller must be specified in socket.handshake.query.socket
|
|
360
|
+
*
|
|
361
|
+
* @param {Array} data - [eventName, payload] tuple
|
|
362
|
+
* @param {string} data[0] - Action/event name to call
|
|
363
|
+
* @param {*} data[1] - Payload to pass to action
|
|
364
|
+
* @param {Socket} socket - Socket.IO socket instance
|
|
365
|
+
* @param {Server} io - Socket.IO server instance
|
|
366
|
+
* @returns {Promise<void>}
|
|
367
|
+
* @throws {Error} If controller not found or action fails
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* // Client emits: socket.emit('updateBoard', { boardId: 123 })
|
|
371
|
+
* // Routes to: app/sockets/BoardSocket.js -> updateBoard(payload, socket, io)
|
|
372
|
+
* await this.load(['updateBoard', { boardId: 123 }], socket, io);
|
|
373
|
+
*/
|
|
104
374
|
async load(data, socket, io){
|
|
105
|
-
|
|
375
|
+
try {
|
|
376
|
+
// Input validation
|
|
377
|
+
validateSocketData(data);
|
|
378
|
+
|
|
379
|
+
if (!socket || typeof socket !== 'object') {
|
|
380
|
+
throw new TypeError('Socket parameter must be a valid Socket.IO socket object');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!io || typeof io !== 'object') {
|
|
384
|
+
throw new TypeError('IO parameter must be a valid Socket.IO server object');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Validate socket has required properties
|
|
388
|
+
if (!socket.handshake || !socket.handshake.query) {
|
|
389
|
+
throw new Error('Socket handshake.query is required');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const controllerName = socket.handshake.query.socket;
|
|
393
|
+
|
|
394
|
+
if (!controllerName) {
|
|
395
|
+
logger.warn({
|
|
396
|
+
code: 'MC_SOCKET_NO_CONTROLLER',
|
|
397
|
+
message: 'Socket connection missing controller name in handshake.query.socket'
|
|
398
|
+
});
|
|
399
|
+
socket.emit(SOCKET_EVENTS.ERROR, {
|
|
400
|
+
error: 'Missing controller name',
|
|
401
|
+
code: 'MC_SOCKET_NO_CONTROLLER'
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
validateSocketIdentifier(controllerName, 'controller');
|
|
407
|
+
|
|
408
|
+
const controller = jsUcfirst(controllerName);
|
|
409
|
+
|
|
106
410
|
if(controller){
|
|
107
411
|
try{
|
|
108
412
|
// Try case-sensitive first (PascalCase), then fallback to camelCase for cross-platform compatibility
|
|
109
|
-
|
|
110
|
-
|
|
413
|
+
const moduleName = path.join(this._baseurl, 'app', 'sockets', controller + 'Socket');
|
|
414
|
+
let BoardSocket;
|
|
111
415
|
try {
|
|
112
416
|
BoardSocket = require(moduleName);
|
|
113
417
|
} catch (e) {
|
|
114
418
|
// If PascalCase fails (Linux case-sensitive), try camelCase
|
|
115
419
|
if (e.code === 'MODULE_NOT_FOUND') {
|
|
116
|
-
|
|
420
|
+
const camelCaseModuleName = path.join(this._baseurl, 'app', 'sockets', controller.charAt(0).toLowerCase() + controller.slice(1) + 'Socket');
|
|
117
421
|
BoardSocket = require(camelCaseModuleName);
|
|
118
422
|
} else {
|
|
119
423
|
throw e;
|
|
120
424
|
}
|
|
121
425
|
}
|
|
122
|
-
|
|
426
|
+
const bs = new BoardSocket();
|
|
123
427
|
bs.request = socket.request;
|
|
124
428
|
bs.response = socket.response;
|
|
125
429
|
bs.namespace = (controller).toLowerCase();
|
|
@@ -136,22 +440,83 @@ class MasterSocket{
|
|
|
136
440
|
await bs.callBeforeAction(data);
|
|
137
441
|
}
|
|
138
442
|
|
|
443
|
+
// Check if action method exists
|
|
444
|
+
if (typeof bs[data[0]] !== 'function') {
|
|
445
|
+
throw new Error(`Action '${data[0]}' not found in socket controller ${controller}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
139
448
|
bs[data[0]](data[1], socket, io);
|
|
140
449
|
}
|
|
141
450
|
catch(ex){
|
|
142
|
-
|
|
451
|
+
logger.error({
|
|
452
|
+
code: 'MC_SOCKET_LOAD_ERROR',
|
|
453
|
+
message: 'Socket controller load failed',
|
|
454
|
+
controller,
|
|
455
|
+
action: data[0],
|
|
456
|
+
error: ex.message,
|
|
457
|
+
stack: ex.stack
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Send error back to client
|
|
461
|
+
socket.emit(SOCKET_EVENTS.ERROR, {
|
|
462
|
+
error: 'Controller action failed',
|
|
463
|
+
code: 'MC_SOCKET_LOAD_ERROR',
|
|
464
|
+
action: data[0]
|
|
465
|
+
});
|
|
143
466
|
}
|
|
144
467
|
|
|
145
468
|
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
// Validation or unexpected errors
|
|
471
|
+
logger.error({
|
|
472
|
+
code: 'MC_SOCKET_VALIDATION_ERROR',
|
|
473
|
+
message: 'Socket load validation failed',
|
|
474
|
+
error: error.message,
|
|
475
|
+
stack: error.stack
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
if (socket && typeof socket.emit === 'function') {
|
|
479
|
+
socket.emit(SOCKET_EVENTS.ERROR, {
|
|
480
|
+
error: error.message,
|
|
481
|
+
code: 'MC_SOCKET_VALIDATION_ERROR'
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
146
485
|
}
|
|
147
486
|
}
|
|
148
487
|
|
|
149
488
|
module.exports = { MasterSocket };
|
|
150
489
|
|
|
151
|
-
|
|
490
|
+
/**
|
|
491
|
+
* Check if value is a plain object
|
|
492
|
+
*
|
|
493
|
+
* @param {*} item - Value to check
|
|
494
|
+
* @returns {boolean} True if plain object, false otherwise
|
|
495
|
+
* @example
|
|
496
|
+
* isObject({}) // true
|
|
497
|
+
* isObject([]) // false
|
|
498
|
+
* isObject(null) // false
|
|
499
|
+
*/
|
|
152
500
|
function isObject(item) {
|
|
153
501
|
return (item && typeof item === 'object' && !Array.isArray(item));
|
|
154
502
|
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Deep merge two objects
|
|
506
|
+
*
|
|
507
|
+
* Recursively merges source into target, creating a new object.
|
|
508
|
+
* Arrays and primitives are replaced, objects are merged.
|
|
509
|
+
*
|
|
510
|
+
* @param {Object} target - Target object
|
|
511
|
+
* @param {Object} source - Source object to merge
|
|
512
|
+
* @returns {Object} New merged object
|
|
513
|
+
*
|
|
514
|
+
* @example
|
|
515
|
+
* const a = { cors: { origin: 'a' }, port: 3000 };
|
|
516
|
+
* const b = { cors: { credentials: true }, host: 'localhost' };
|
|
517
|
+
* mergeDeep(a, b);
|
|
518
|
+
* // Returns: { cors: { origin: 'a', credentials: true }, port: 3000, host: 'localhost' }
|
|
519
|
+
*/
|
|
155
520
|
function mergeDeep(target, source) {
|
|
156
521
|
const output = Object.assign({}, target);
|
|
157
522
|
if (isObject(target) && isObject(source)) {
|