@syntropysoft/syntropyfront 0.2.6 → 0.4.0

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/dist/index.js CHANGED
@@ -1,426 +1,2885 @@
1
1
  /**
2
- * BreadcrumbManager - Gestiona breadcrumbs
3
- * Responsabilidad única: Almacenar y gestionar breadcrumbs
2
+ * BreadcrumbStore - Almacén de huellas del usuario
3
+ * Mantiene un historial de las últimas acciones del usuario
4
4
  */
5
- class BreadcrumbManager {
6
- constructor() {
5
+ class BreadcrumbStore {
6
+ constructor(maxBreadcrumbs = 25) {
7
+ this.maxBreadcrumbs = maxBreadcrumbs;
7
8
  this.breadcrumbs = [];
9
+ this.agent = null;
10
+ }
11
+
12
+ /**
13
+ * Configura el agent para envío automático
14
+ * @param {Object} agent - Instancia del agent
15
+ */
16
+ setAgent(agent) {
17
+ this.agent = agent;
18
+ }
19
+
20
+ /**
21
+ * Configura el tamaño máximo de breadcrumbs
22
+ * @param {number} maxBreadcrumbs - Nuevo tamaño máximo
23
+ */
24
+ setMaxBreadcrumbs(maxBreadcrumbs) {
25
+ this.maxBreadcrumbs = maxBreadcrumbs;
26
+
27
+ // Si el nuevo tamaño es menor, eliminar breadcrumbs excedentes
28
+ if (this.breadcrumbs.length > this.maxBreadcrumbs) {
29
+ this.breadcrumbs = this.breadcrumbs.slice(-this.maxBreadcrumbs);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Obtiene el tamaño máximo actual
35
+ * @returns {number} Tamaño máximo de breadcrumbs
36
+ */
37
+ getMaxBreadcrumbs() {
38
+ return this.maxBreadcrumbs;
8
39
  }
9
40
 
10
- add(category, message, data = {}) {
41
+ /**
42
+ * Añade un breadcrumb a la lista
43
+ * @param {Object} crumb - El breadcrumb a añadir
44
+ * @param {string} crumb.category - Categoría del evento (ui, network, error, etc.)
45
+ * @param {string} crumb.message - Mensaje descriptivo
46
+ * @param {Object} [crumb.data] - Datos adicionales opcionales
47
+ */
48
+ add(crumb) {
11
49
  const breadcrumb = {
12
- category,
13
- message,
14
- data,
15
- timestamp: new Date().toISOString()
50
+ ...crumb,
51
+ timestamp: new Date().toISOString(),
16
52
  };
53
+
54
+ if (this.breadcrumbs.length >= this.maxBreadcrumbs) {
55
+ this.breadcrumbs.shift(); // Elimina el más antiguo
56
+ }
17
57
 
18
58
  this.breadcrumbs.push(breadcrumb);
19
- return breadcrumb;
59
+
60
+ // Callback opcional para logging
61
+ if (this.onBreadcrumbAdded) {
62
+ this.onBreadcrumbAdded(breadcrumb);
63
+ }
64
+
65
+ // Enviar al agent si está configurado y habilitado
66
+ if (this.agent && this.agent.isEnabled) {
67
+ try {
68
+ this.agent.sendBreadcrumbs([breadcrumb]);
69
+ } catch (error) {
70
+ console.warn('SyntropyFront: Error enviando breadcrumb al agent:', error);
71
+ }
72
+ }
20
73
  }
21
74
 
75
+ /**
76
+ * Devuelve todos los breadcrumbs
77
+ * @returns {Array} Copia de todos los breadcrumbs
78
+ */
22
79
  getAll() {
23
- return this.breadcrumbs;
80
+ return [...this.breadcrumbs];
24
81
  }
25
82
 
83
+ /**
84
+ * Limpia todos los breadcrumbs
85
+ */
26
86
  clear() {
27
87
  this.breadcrumbs = [];
28
88
  }
29
89
 
30
- getCount() {
31
- return this.breadcrumbs.length;
90
+ /**
91
+ * Obtiene breadcrumbs por categoría
92
+ * @param {string} category - Categoría a filtrar
93
+ * @returns {Array} Breadcrumbs de la categoría especificada
94
+ */
95
+ getByCategory(category) {
96
+ return this.breadcrumbs.filter(b => b.category === category);
32
97
  }
33
98
  }
34
99
 
100
+ // Instancia singleton
101
+ const breadcrumbStore = new BreadcrumbStore();
102
+
35
103
  /**
36
- * ErrorManager - Gestiona errores
37
- * Responsabilidad única: Formatear y gestionar errores
104
+ * ConfigurationManager - Maneja la configuración del Agent
105
+ * Responsabilidad única: Gestionar configuración y validación
38
106
  */
39
- class ErrorManager {
107
+ class ConfigurationManager {
40
108
  constructor() {
41
- this.errors = [];
109
+ this.endpoint = null;
110
+ this.headers = {
111
+ 'Content-Type': 'application/json'
112
+ };
113
+ this.batchSize = 10;
114
+ this.batchTimeout = null;
115
+ this.isEnabled = false;
116
+ this.sendBreadcrumbs = false;
117
+ this.encrypt = null;
118
+ this.usePersistentBuffer = false;
119
+ this.maxRetries = 5;
120
+ this.baseDelay = 1000;
121
+ this.maxDelay = 30000;
42
122
  }
43
123
 
44
- send(error, context = {}) {
45
- const errorData = {
46
- message: error.message,
47
- stack: error.stack,
48
- context,
49
- timestamp: new Date().toISOString()
50
- };
124
+ /**
125
+ * Configura el manager
126
+ * @param {Object} config - Configuración
127
+ */
128
+ configure(config) {
129
+ this.endpoint = config.endpoint;
130
+ this.headers = { ...this.headers, ...config.headers };
131
+ this.batchSize = config.batchSize || this.batchSize;
132
+ this.batchTimeout = config.batchTimeout;
133
+ this.isEnabled = !!config.endpoint;
134
+ this.encrypt = config.encrypt || null;
135
+ this.usePersistentBuffer = config.usePersistentBuffer === true;
136
+ this.maxRetries = config.maxRetries || this.maxRetries;
51
137
 
52
- this.errors.push(errorData);
53
- return errorData;
138
+ // Lógica simple: si hay batchTimeout = enviar breadcrumbs, sino = solo errores
139
+ this.sendBreadcrumbs = !!config.batchTimeout;
54
140
  }
55
141
 
56
- getAll() {
57
- return this.errors;
142
+ /**
143
+ * Verifica si el agent está habilitado
144
+ */
145
+ isAgentEnabled() {
146
+ return this.isEnabled;
58
147
  }
59
148
 
60
- clear() {
61
- this.errors = [];
149
+ /**
150
+ * Verifica si debe enviar breadcrumbs
151
+ */
152
+ shouldSendBreadcrumbs() {
153
+ return this.sendBreadcrumbs;
62
154
  }
63
155
 
64
- getCount() {
65
- return this.errors.length;
156
+ /**
157
+ * Obtiene la configuración actual
158
+ */
159
+ getConfig() {
160
+ return {
161
+ endpoint: this.endpoint,
162
+ headers: this.headers,
163
+ batchSize: this.batchSize,
164
+ batchTimeout: this.batchTimeout,
165
+ isEnabled: this.isEnabled,
166
+ sendBreadcrumbs: this.sendBreadcrumbs,
167
+ encrypt: this.encrypt,
168
+ usePersistentBuffer: this.usePersistentBuffer,
169
+ maxRetries: this.maxRetries,
170
+ baseDelay: this.baseDelay,
171
+ maxDelay: this.maxDelay
172
+ };
66
173
  }
67
174
  }
68
175
 
69
176
  /**
70
- * Logger - Hace logging solo en errores
71
- * Responsabilidad única: Mostrar mensajes solo cuando hay errores
177
+ * QueueManager - Maneja la cola de envío y batching
178
+ * Responsabilidad única: Gestionar cola de items y batching
72
179
  */
73
- class Logger {
74
- constructor() {
75
- this.isSilent = true; // Por defecto silente
180
+ class QueueManager {
181
+ constructor(configManager) {
182
+ this.config = configManager;
183
+ this.queue = [];
184
+ this.batchTimer = null;
185
+ this.flushCallback = null; // Callback interno para flush automático
76
186
  }
77
187
 
78
- log(message, data = null) {
79
- // No loggear nada en modo silente
80
- if (this.isSilent) return;
81
-
82
- if (data) {
83
- console.log(message, data);
84
- } else {
85
- console.log(message);
188
+ /**
189
+ * Añade un item a la cola
190
+ * @param {Object} item - Item a añadir
191
+ */
192
+ add(item) {
193
+ this.queue.push(item);
194
+
195
+ // Enviar inmediatamente si alcanza el tamaño del batch
196
+ if (this.queue.length >= this.config.batchSize) {
197
+ this.flush(this.flushCallback);
198
+ } else if (this.config.batchSize && this.config.batchTimeout && !this.batchTimer) {
199
+ // Solo programar timeout si batchTimeout está configurado
200
+ this.batchTimer = setTimeout(() => {
201
+ this.flush(this.flushCallback);
202
+ }, this.config.batchTimeout);
86
203
  }
87
204
  }
88
205
 
89
- error(message, data = null) {
90
- // SIEMPRE loggear errores (ignora modo silencioso)
91
- if (data) {
92
- console.error(message, data);
93
- } else {
94
- console.error(message);
95
- }
206
+ /**
207
+ * Obtiene todos los items de la cola
208
+ */
209
+ getAll() {
210
+ return [...this.queue];
96
211
  }
97
212
 
98
- warn(message, data = null) {
99
- // Solo warnings importantes
100
- if (data) {
101
- console.warn(message, data);
102
- } else {
103
- console.warn(message);
213
+ /**
214
+ * Limpia la cola
215
+ */
216
+ clear() {
217
+ this.queue = [];
218
+ this.clearTimer();
219
+ }
220
+
221
+ /**
222
+ * Limpia el timer
223
+ */
224
+ clearTimer() {
225
+ if (this.batchTimer) {
226
+ clearTimeout(this.batchTimer);
227
+ this.batchTimer = null;
104
228
  }
105
229
  }
106
230
 
107
- // Método para activar logging (solo para debug)
108
- enableLogging() {
109
- this.isSilent = false;
231
+ /**
232
+ * Obtiene el tamaño de la cola
233
+ */
234
+ getSize() {
235
+ return this.queue.length;
236
+ }
237
+
238
+ /**
239
+ * Verifica si la cola está vacía
240
+ */
241
+ isEmpty() {
242
+ return this.queue.length === 0;
110
243
  }
111
244
 
112
- // Método para desactivar logging
113
- disableLogging() {
114
- this.isSilent = true;
245
+ /**
246
+ * Flush de la cola (método que será llamado por el Agent)
247
+ * @param {Function} flushCallback - Callback para procesar los items
248
+ */
249
+ async flush(flushCallback) {
250
+ if (this.queue.length === 0) return;
251
+
252
+ const itemsToSend = [...this.queue];
253
+ this.queue = [];
254
+ this.clearTimer();
255
+
256
+ if (flushCallback) {
257
+ await flushCallback(itemsToSend);
258
+ }
115
259
  }
116
260
  }
117
261
 
118
262
  /**
119
- * SyntropyFront - Observability library with automatic capture
120
- * Single responsibility: Automatically capture events and send errors with context
263
+ * RetryManager - Maneja el sistema de reintentos
264
+ * Responsabilidad única: Gestionar reintentos con backoff exponencial
121
265
  */
266
+ class RetryManager {
267
+ constructor(configManager) {
268
+ this.config = configManager;
269
+ this.retryQueue = [];
270
+ this.retryTimer = null;
271
+ }
122
272
 
123
- class SyntropyFront {
124
- constructor() {
125
- // Basic managers
126
- this.breadcrumbManager = new BreadcrumbManager();
127
- this.errorManager = new ErrorManager();
128
- this.logger = new Logger();
129
-
130
- // Default configuration
131
- this.maxEvents = 50;
132
- this.fetchConfig = null; // Complete fetch configuration
133
- this.onErrorCallback = null; // User-defined error handler
134
- this.isActive = false;
135
-
136
- // Automatic capture
137
- this.originalHandlers = {};
273
+ /**
274
+ * Añade items a la cola de reintentos
275
+ * @param {Array} items - Items a reintentar
276
+ * @param {number} retryCount - Número de reintento
277
+ * @param {number} persistentId - ID en buffer persistente (opcional)
278
+ */
279
+ addToRetryQueue(items, retryCount = 1, persistentId = null) {
280
+ const delay = Math.min(this.config.baseDelay * Math.pow(2, retryCount - 1), this.config.maxDelay);
138
281
 
139
- // Auto-initialize
140
- this.init();
282
+ this.retryQueue.push({
283
+ items,
284
+ retryCount,
285
+ persistentId,
286
+ nextRetry: Date.now() + delay
287
+ });
288
+
289
+ this.scheduleRetry();
141
290
  }
142
291
 
143
- init() {
144
- this.isActive = true;
145
-
146
- // Configure automatic capture immediately
147
- this.setupAutomaticCapture();
148
-
149
- console.log('🚀 SyntropyFront: Initialized with automatic capture');
292
+ /**
293
+ * Programa el próximo reintento
294
+ */
295
+ scheduleRetry() {
296
+ if (this.retryTimer) return;
297
+
298
+ const nextItem = this.retryQueue.find(item => item.nextRetry <= Date.now());
299
+ if (!nextItem) return;
300
+
301
+ this.retryTimer = setTimeout(() => {
302
+ this.processRetryQueue();
303
+ }, Math.max(0, nextItem.nextRetry - Date.now()));
150
304
  }
151
305
 
152
306
  /**
153
- * Configure SyntropyFront
154
- * @param {Object} config - Configuration
155
- * @param {number} config.maxEvents - Maximum number of events to store
156
- * @param {Object} config.fetch - Complete fetch configuration
157
- * @param {string} config.fetch.url - Endpoint URL
158
- * @param {Object} config.fetch.options - Fetch options (headers, method, etc.)
159
- * @param {Function} config.onError - User-defined error handler callback
307
+ * Procesa la cola de reintentos
308
+ * @param {Function} sendCallback - Callback para enviar items
309
+ * @param {Function} removePersistentCallback - Callback para remover del buffer persistente
160
310
  */
161
- configure(config = {}) {
162
- this.maxEvents = config.maxEvents || this.maxEvents;
163
- this.fetchConfig = config.fetch;
164
- this.onErrorCallback = config.onError;
311
+ async processRetryQueue(sendCallback, removePersistentCallback) {
312
+ this.retryTimer = null;
313
+
314
+ const now = Date.now();
315
+ const itemsToRetry = this.retryQueue.filter(item => item.nextRetry <= now);
165
316
 
166
- if (this.onErrorCallback) {
167
- console.log(`✅ SyntropyFront: Configured - maxEvents: ${this.maxEvents}, custom error handler`);
168
- } else if (this.fetchConfig) {
169
- console.log(`✅ SyntropyFront: Configured - maxEvents: ${this.maxEvents}, endpoint: ${this.fetchConfig.url}`);
170
- } else {
171
- console.log(`✅ SyntropyFront: Configured - maxEvents: ${this.maxEvents}, console only`);
317
+ for (const item of itemsToRetry) {
318
+ try {
319
+ if (sendCallback) {
320
+ await sendCallback(item.items);
321
+ }
322
+
323
+ // ✅ Éxito: remover de cola de reintentos
324
+ this.retryQueue = this.retryQueue.filter(q => q !== item);
325
+
326
+ // Remover del buffer persistente si existe
327
+ if (item.persistentId && removePersistentCallback) {
328
+ await removePersistentCallback(item.persistentId);
329
+ }
330
+
331
+ console.log(`SyntropyFront: Reintento exitoso después de ${item.retryCount} intentos`);
332
+ } catch (error) {
333
+ console.warn(`SyntropyFront: Reintento ${item.retryCount} falló:`, error);
334
+
335
+ if (item.retryCount >= this.config.maxRetries) {
336
+ // ❌ Máximo de reintentos alcanzado
337
+ this.retryQueue = this.retryQueue.filter(q => q !== item);
338
+ console.error('SyntropyFront: Item excedió máximo de reintentos, datos perdidos');
339
+ } else {
340
+ // Programar próximo reintento
341
+ item.retryCount++;
342
+ item.nextRetry = Date.now() + Math.min(
343
+ this.config.baseDelay * Math.pow(2, item.retryCount - 1),
344
+ this.config.maxDelay
345
+ );
346
+ }
347
+ }
348
+ }
349
+
350
+ // Programar próximo reintento si quedan items
351
+ if (this.retryQueue.length > 0) {
352
+ this.scheduleRetry();
172
353
  }
173
354
  }
174
355
 
175
356
  /**
176
- * Configure automatic event capture
357
+ * Limpia la cola de reintentos
177
358
  */
178
- setupAutomaticCapture() {
179
- if (typeof window === 'undefined') return;
359
+ clear() {
360
+ this.retryQueue = [];
361
+ this.clearTimer();
362
+ }
180
363
 
181
- // Capture clicks
182
- this.setupClickCapture();
183
-
184
- // Capture errors
185
- this.setupErrorCapture();
186
-
187
- // Capture HTTP calls
188
- this.setupHttpCapture();
189
-
190
- // Capture console logs
191
- this.setupConsoleCapture();
364
+ /**
365
+ * Limpia el timer
366
+ */
367
+ clearTimer() {
368
+ if (this.retryTimer) {
369
+ clearTimeout(this.retryTimer);
370
+ this.retryTimer = null;
371
+ }
192
372
  }
193
373
 
194
374
  /**
195
- * Capture user clicks
375
+ * Obtiene el tamaño de la cola de reintentos
196
376
  */
197
- setupClickCapture() {
198
- const clickHandler = (event) => {
199
- const element = event.target;
200
- this.addBreadcrumb('user', 'click', {
201
- element: element.tagName,
202
- id: element.id,
203
- className: element.className,
204
- x: event.clientX,
205
- y: event.clientY
206
- });
207
- };
377
+ getSize() {
378
+ return this.retryQueue.length;
379
+ }
380
+
381
+ /**
382
+ * Verifica si la cola de reintentos está vacía
383
+ */
384
+ isEmpty() {
385
+ return this.retryQueue.length === 0;
386
+ }
387
+ }
208
388
 
209
- document.addEventListener('click', clickHandler);
389
+ /**
390
+ * RobustSerializer - Serializador robusto que maneja referencias circulares
391
+ * Implementa una solución similar a flatted pero sin dependencias externas
392
+ */
393
+ class RobustSerializer {
394
+ constructor() {
395
+ this.seen = new WeakSet();
396
+ this.circularRefs = new Map();
397
+ this.refCounter = 0;
210
398
  }
211
399
 
212
400
  /**
213
- * Automatically capture errors
401
+ * Serializa un objeto de forma segura, manejando referencias circulares
402
+ * @param {any} obj - Objeto a serializar
403
+ * @returns {string} JSON string seguro
214
404
  */
215
- setupErrorCapture() {
216
- // Save original handlers
217
- this.originalHandlers.onerror = window.onerror;
218
- this.originalHandlers.onunhandledrejection = window.onunhandledrejection;
405
+ serialize(obj) {
406
+ try {
407
+ // Reset state
408
+ this.seen = new WeakSet();
409
+ this.circularRefs = new Map();
410
+ this.refCounter = 0;
219
411
 
220
- // Intercept errors
221
- window.onerror = (message, source, lineno, colno, error) => {
222
- const errorPayload = {
223
- type: 'uncaught_exception',
224
- error: { message, source, lineno, colno, stack: error?.stack },
225
- breadcrumbs: this.getBreadcrumbs(),
412
+ // Serializar con manejo de referencias circulares
413
+ const safeObj = this.makeSerializable(obj);
414
+
415
+ // Convertir a JSON
416
+ return JSON.stringify(safeObj);
417
+ } catch (error) {
418
+ console.error('SyntropyFront: Error en serialización robusta:', error);
419
+
420
+ // Fallback: intentar serialización básica con información de error
421
+ return JSON.stringify({
422
+ __serializationError: true,
423
+ error: error.message,
424
+ originalType: typeof obj,
425
+ isObject: obj !== null && typeof obj === 'object',
226
426
  timestamp: new Date().toISOString()
427
+ });
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Hace un objeto serializable, manejando referencias circulares
433
+ * @param {any} obj - Objeto a procesar
434
+ * @param {string} path - Ruta actual en el objeto
435
+ * @returns {any} Objeto serializable
436
+ */
437
+ makeSerializable(obj, path = '') {
438
+ // Casos primitivos
439
+ if (obj === null || obj === undefined) {
440
+ return obj;
441
+ }
442
+
443
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
444
+ return obj;
445
+ }
446
+
447
+ // Casos especiales
448
+ if (obj instanceof Date) {
449
+ return {
450
+ __type: 'Date',
451
+ value: obj.toISOString()
227
452
  };
453
+ }
228
454
 
229
- this.handleError(errorPayload);
230
-
231
- // Call original handler
232
- if (this.originalHandlers.onerror) {
233
- return this.originalHandlers.onerror(message, source, lineno, colno, error);
234
- }
235
-
236
- return false;
237
- };
455
+ if (obj instanceof Error) {
456
+ return {
457
+ __type: 'Error',
458
+ name: obj.name,
459
+ message: obj.message,
460
+ stack: obj.stack,
461
+ cause: obj.cause ? this.makeSerializable(obj.cause, `${path}.cause`) : undefined
462
+ };
463
+ }
238
464
 
239
- // Intercept rejected promises
240
- window.onunhandledrejection = (event) => {
241
- const errorPayload = {
242
- type: 'unhandled_rejection',
243
- error: {
244
- message: event.reason?.message || 'Promise rejection without message',
245
- stack: event.reason?.stack,
246
- },
247
- breadcrumbs: this.getBreadcrumbs(),
248
- timestamp: new Date().toISOString()
465
+ if (obj instanceof RegExp) {
466
+ return {
467
+ __type: 'RegExp',
468
+ source: obj.source,
469
+ flags: obj.flags
249
470
  };
471
+ }
250
472
 
251
- this.handleError(errorPayload);
252
-
253
- // Call original handler
254
- if (this.originalHandlers.onunhandledrejection) {
255
- this.originalHandlers.onunhandledrejection(event);
473
+ // Arrays
474
+ if (Array.isArray(obj)) {
475
+ // Verificar referencia circular
476
+ if (this.seen.has(obj)) {
477
+ const refId = this.circularRefs.get(obj);
478
+ return {
479
+ __circular: true,
480
+ refId
481
+ };
482
+ }
483
+
484
+ this.seen.add(obj);
485
+ const refId = `ref_${++this.refCounter}`;
486
+ this.circularRefs.set(obj, refId);
487
+
488
+ return obj.map((item, index) =>
489
+ this.makeSerializable(item, `${path}[${index}]`)
490
+ );
491
+ }
492
+
493
+ // Objetos
494
+ if (typeof obj === 'object') {
495
+ // Verificar referencia circular
496
+ if (this.seen.has(obj)) {
497
+ const refId = this.circularRefs.get(obj);
498
+ return {
499
+ __circular: true,
500
+ refId
501
+ };
502
+ }
503
+
504
+ this.seen.add(obj);
505
+ const refId = `ref_${++this.refCounter}`;
506
+ this.circularRefs.set(obj, refId);
507
+
508
+ const result = {};
509
+
510
+ // Procesar propiedades del objeto
511
+ for (const key in obj) {
512
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
513
+ try {
514
+ const value = obj[key];
515
+ const safeValue = this.makeSerializable(value, `${path}.${key}`);
516
+ result[key] = safeValue;
517
+ } catch (error) {
518
+ // Si falla la serialización de una propiedad, la omitimos
519
+ result[key] = {
520
+ __serializationError: true,
521
+ error: error.message,
522
+ propertyName: key
523
+ };
524
+ }
525
+ }
526
+ }
527
+
528
+ // Procesar símbolos si están disponibles
529
+ if (Object.getOwnPropertySymbols) {
530
+ const symbols = Object.getOwnPropertySymbols(obj);
531
+ for (const symbol of symbols) {
532
+ try {
533
+ const value = obj[symbol];
534
+ const safeValue = this.makeSerializable(value, `${path}[Symbol(${symbol.description})]`);
535
+ result[`__symbol_${symbol.description || 'anonymous'}`] = safeValue;
536
+ } catch (error) {
537
+ result[`__symbol_${symbol.description || 'anonymous'}`] = {
538
+ __serializationError: true,
539
+ error: error.message,
540
+ symbolName: symbol.description || 'anonymous'
541
+ };
542
+ }
543
+ }
256
544
  }
545
+
546
+ return result;
547
+ }
548
+
549
+ // Funciones y otros tipos
550
+ if (typeof obj === 'function') {
551
+ return {
552
+ __type: 'Function',
553
+ name: obj.name || 'anonymous',
554
+ length: obj.length,
555
+ toString: `${obj.toString().substring(0, 200) }...`
556
+ };
557
+ }
558
+
559
+ // Fallback para otros tipos
560
+ return {
561
+ __type: 'Unknown',
562
+ constructor: obj.constructor ? obj.constructor.name : 'Unknown',
563
+ toString: `${String(obj).substring(0, 200) }...`
257
564
  };
258
565
  }
259
566
 
260
567
  /**
261
- * Capture HTTP calls
568
+ * Deserializa un objeto serializado con referencias circulares
569
+ * @param {string} jsonString - JSON string a deserializar
570
+ * @returns {any} Objeto deserializado
262
571
  */
263
- setupHttpCapture() {
264
- // Intercept fetch
265
- const originalFetch = window.fetch;
266
- window.fetch = (...args) => {
267
- const [url, options] = args;
268
-
269
- this.addBreadcrumb('http', 'fetch', {
270
- url,
271
- method: options?.method || 'GET'
272
- });
273
-
274
- return originalFetch(...args).then(response => {
275
- this.addBreadcrumb('http', 'fetch_response', {
276
- url,
277
- status: response.status
278
- });
279
- return response;
280
- }).catch(error => {
281
- this.addBreadcrumb('http', 'fetch_error', {
282
- url,
283
- error: error.message
284
- });
285
- throw error;
286
- });
287
- };
572
+ deserialize(jsonString) {
573
+ try {
574
+ const parsed = JSON.parse(jsonString);
575
+ return this.restoreCircularRefs(parsed);
576
+ } catch (error) {
577
+ console.error('SyntropyFront: Error en deserialización:', error);
578
+ return null;
579
+ }
288
580
  }
289
581
 
290
582
  /**
291
- * Capture console logs
583
+ * Restaura referencias circulares en un objeto deserializado
584
+ * @param {any} obj - Objeto a restaurar
585
+ * @param {Map} refs - Mapa de referencias
586
+ * @returns {any} Objeto con referencias restauradas
292
587
  */
293
- setupConsoleCapture() {
294
- const originalLog = console.log;
295
- const originalError = console.error;
296
- const originalWarn = console.warn;
588
+ restoreCircularRefs(obj, refs = new Map()) {
589
+ if (obj === null || obj === undefined) {
590
+ return obj;
591
+ }
297
592
 
298
- console.log = (...args) => {
299
- this.addBreadcrumb('console', 'log', { message: args.join(' ') });
300
- originalLog.apply(console, args);
301
- };
593
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
594
+ return obj;
595
+ }
302
596
 
303
- console.error = (...args) => {
304
- this.addBreadcrumb('console', 'error', { message: args.join(' ') });
305
- originalError.apply(console, args);
306
- };
597
+ // Restaurar tipos especiales
598
+ if (obj.__type === 'Date') {
599
+ return new Date(obj.value);
600
+ }
307
601
 
308
- console.warn = (...args) => {
309
- this.addBreadcrumb('console', 'warn', { message: args.join(' ') });
310
- originalWarn.apply(console, args);
311
- };
602
+ if (obj.__type === 'Error') {
603
+ const error = new Error(obj.message);
604
+ error.name = obj.name;
605
+ error.stack = obj.stack;
606
+ if (obj.cause) {
607
+ error.cause = this.restoreCircularRefs(obj.cause, refs);
608
+ }
609
+ return error;
610
+ }
611
+
612
+ if (obj.__type === 'RegExp') {
613
+ return new RegExp(obj.source, obj.flags);
614
+ }
615
+
616
+ if (obj.__type === 'Function') {
617
+ // No podemos restaurar funciones completamente, devolvemos info
618
+ return `[Function: ${obj.name}]`;
619
+ }
620
+
621
+ // Arrays
622
+ if (Array.isArray(obj)) {
623
+ const result = [];
624
+ refs.set(obj, result);
625
+
626
+ for (let i = 0; i < obj.length; i++) {
627
+ if (obj[i] && obj[i].__circular) {
628
+ const refId = obj[i].refId;
629
+ if (refs.has(refId)) {
630
+ result[i] = refs.get(refId);
631
+ } else {
632
+ result[i] = null; // Referencia no encontrada
633
+ }
634
+ } else {
635
+ result[i] = this.restoreCircularRefs(obj[i], refs);
636
+ }
637
+ }
638
+
639
+ return result;
640
+ }
641
+
642
+ // Objetos
643
+ if (typeof obj === 'object') {
644
+ const result = {};
645
+ refs.set(obj, result);
646
+
647
+ for (const key in obj) {
648
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
649
+ if (key.startsWith('__')) {
650
+ // Propiedades especiales
651
+ continue;
652
+ }
653
+
654
+ const value = obj[key];
655
+ if (value && value.__circular) {
656
+ const refId = value.refId;
657
+ if (refs.has(refId)) {
658
+ result[key] = refs.get(refId);
659
+ } else {
660
+ result[key] = null; // Referencia no encontrada
661
+ }
662
+ } else {
663
+ result[key] = this.restoreCircularRefs(value, refs);
664
+ }
665
+ }
666
+ }
667
+
668
+ return result;
669
+ }
670
+
671
+ return obj;
312
672
  }
313
673
 
314
674
  /**
315
- * Handle errors - priority: onError callback > fetch > console
675
+ * Serializa de forma segura para logging (versión simplificada)
676
+ * @param {any} obj - Objeto a serializar
677
+ * @returns {string} JSON string seguro para logs
316
678
  */
317
- handleError(errorPayload) {
318
- // Default log
319
- this.logger.error('❌ Error:', errorPayload);
320
-
321
- // Priority 1: User-defined callback (maximum flexibility)
322
- if (this.onErrorCallback) {
323
- try {
324
- this.onErrorCallback(errorPayload);
325
- } catch (callbackError) {
326
- console.warn('SyntropyFront: Error in user callback:', callbackError);
327
- }
328
- return;
329
- }
330
-
331
- // Priority 2: Fetch to endpoint
332
- if (this.fetchConfig) {
333
- this.postToEndpoint(errorPayload);
334
- return;
679
+ serializeForLogging(obj) {
680
+ try {
681
+ return this.serialize(obj);
682
+ } catch (error) {
683
+ return JSON.stringify({
684
+ __logError: true,
685
+ message: 'Error serializando para logging',
686
+ originalError: error.message,
687
+ timestamp: new Date().toISOString()
688
+ });
335
689
  }
336
-
337
- // Priority 3: Console only (default)
338
- // Already logged above
690
+ }
691
+ }
692
+
693
+ // Instancia singleton
694
+ const robustSerializer = new RobustSerializer();
695
+
696
+ /**
697
+ * HttpTransport - Maneja el envío HTTP
698
+ * Responsabilidad única: Gestionar envío HTTP y serialización
699
+ */
700
+ class HttpTransport {
701
+ constructor(configManager) {
702
+ this.config = configManager;
339
703
  }
340
704
 
341
705
  /**
342
- * Post error object using fetch configuration
706
+ * Envía datos al backend
707
+ * @param {Array} items - Items a enviar
343
708
  */
344
- postToEndpoint(errorPayload) {
345
- const { url, options = {} } = this.fetchConfig;
346
-
347
- // Default configuration
348
- const defaultOptions = {
349
- method: 'POST',
350
- headers: {
351
- 'Content-Type': 'application/json',
352
- ...options.headers
353
- },
354
- body: JSON.stringify(errorPayload),
355
- ...options
709
+ async send(items) {
710
+ const payload = {
711
+ timestamp: new Date().toISOString(),
712
+ items
356
713
  };
357
714
 
358
- fetch(url, defaultOptions).catch(error => {
359
- console.warn('SyntropyFront: Error posting to endpoint:', error);
715
+ // SERIALIZACIÓN ROBUSTA: Usar serializador que maneja referencias circulares
716
+ let serializedPayload;
717
+ try {
718
+ serializedPayload = robustSerializer.serialize(payload);
719
+ } catch (error) {
720
+ console.error('SyntropyFront: Error en serialización del payload:', error);
721
+
722
+ // Fallback: intentar serialización básica con información de error
723
+ serializedPayload = JSON.stringify({
724
+ __serializationError: true,
725
+ error: error.message,
726
+ timestamp: new Date().toISOString(),
727
+ itemsCount: items.length,
728
+ fallbackData: 'Serialización falló, datos no enviados'
729
+ });
730
+ }
731
+
732
+ const response = await fetch(this.config.endpoint, {
733
+ method: 'POST',
734
+ headers: this.config.headers,
735
+ body: serializedPayload
360
736
  });
737
+
738
+ if (!response.ok) {
739
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
740
+ }
741
+
742
+ return response.json();
361
743
  }
362
744
 
363
- // Public API
364
- addBreadcrumb(category, message, data = {}) {
365
- if (!this.isActive) return;
366
-
367
- const breadcrumb = this.breadcrumbManager.add(category, message, data);
368
-
369
- // Keep only the last maxEvents
370
- const breadcrumbs = this.breadcrumbManager.getAll();
371
- if (breadcrumbs.length > this.maxEvents) {
372
- this.breadcrumbManager.clear();
373
- breadcrumbs.slice(-this.maxEvents).forEach(b => this.breadcrumbManager.add(b.category, b.message, b.data));
745
+ /**
746
+ * Aplica encriptación si está configurada
747
+ * @param {*} data - Datos a encriptar
748
+ */
749
+ applyEncryption(data) {
750
+ if (this.config.encrypt) {
751
+ return this.config.encrypt(data);
374
752
  }
375
-
376
- return breadcrumb;
753
+ return data;
377
754
  }
378
755
 
379
- getBreadcrumbs() {
380
- return this.breadcrumbManager.getAll();
756
+ /**
757
+ * Verifica si el transport está configurado
758
+ */
759
+ isConfigured() {
760
+ return !!this.config.endpoint;
381
761
  }
762
+ }
382
763
 
383
- clearBreadcrumbs() {
384
- this.breadcrumbManager.clear();
764
+ /**
765
+ * DatabaseConfigManager - Maneja la configuración de IndexedDB
766
+ * Responsabilidad única: Validar y gestionar la configuración de la base de datos
767
+ */
768
+ class DatabaseConfigManager {
769
+ constructor(dbName, dbVersion, storeName) {
770
+ this.dbName = dbName;
771
+ this.dbVersion = dbVersion;
772
+ this.storeName = storeName;
385
773
  }
386
774
 
387
- sendError(error, context = {}) {
388
- if (!this.isActive) return;
389
-
390
- const errorData = this.errorManager.send(error, context);
391
- const errorPayload = {
392
- ...errorData,
393
- breadcrumbs: this.getBreadcrumbs(),
775
+ /**
776
+ * Valida que la configuración sea correcta
777
+ * @returns {Object} Resultado de validación
778
+ */
779
+ validateConfig() {
780
+ const validationResult = {
781
+ isValid: true,
782
+ errors: [],
394
783
  timestamp: new Date().toISOString()
395
784
  };
396
-
397
- this.handleError(errorPayload);
398
- return errorData;
399
- }
400
785
 
401
- getErrors() {
402
- return this.errorManager.getAll();
403
- }
786
+ if (!this.dbName || typeof this.dbName !== 'string') {
787
+ validationResult.isValid = false;
788
+ validationResult.errors.push('dbName debe ser un string no vacío');
789
+ }
790
+
791
+ if (!this.dbVersion || typeof this.dbVersion !== 'number' || this.dbVersion < 1) {
792
+ validationResult.isValid = false;
793
+ validationResult.errors.push('dbVersion debe ser un número mayor a 0');
794
+ }
795
+
796
+ if (!this.storeName || typeof this.storeName !== 'string') {
797
+ validationResult.isValid = false;
798
+ validationResult.errors.push('storeName debe ser un string no vacío');
799
+ }
404
800
 
405
- clearErrors() {
406
- this.errorManager.clear();
801
+ return validationResult;
407
802
  }
408
803
 
409
- // Utility methods
410
- getStats() {
804
+ /**
805
+ * Verifica si IndexedDB está disponible en el entorno
806
+ * @returns {Object} Resultado de disponibilidad
807
+ */
808
+ checkIndexedDBAvailability() {
809
+ const availabilityResult = {
810
+ isAvailable: false,
811
+ reason: null,
812
+ timestamp: new Date().toISOString()
813
+ };
814
+
815
+ if (typeof window === 'undefined') {
816
+ availabilityResult.reason = 'No estamos en un entorno de browser';
817
+ return availabilityResult;
818
+ }
819
+
820
+ if (!window.indexedDB) {
821
+ availabilityResult.reason = 'IndexedDB no está disponible en este browser';
822
+ return availabilityResult;
823
+ }
824
+
825
+ availabilityResult.isAvailable = true;
826
+ return availabilityResult;
827
+ }
828
+
829
+ /**
830
+ * Obtiene la configuración actual
831
+ * @returns {Object} Configuración
832
+ */
833
+ getConfig() {
834
+ return {
835
+ dbName: this.dbName,
836
+ dbVersion: this.dbVersion,
837
+ storeName: this.storeName
838
+ };
839
+ }
840
+
841
+ /**
842
+ * Crea la configuración del object store
843
+ * @returns {Object} Configuración del store
844
+ */
845
+ getStoreConfig() {
846
+ return {
847
+ keyPath: 'id',
848
+ autoIncrement: true
849
+ };
850
+ }
851
+ }
852
+
853
+ /**
854
+ * DatabaseConnectionManager - Maneja la conexión con IndexedDB
855
+ * Responsabilidad única: Gestionar la apertura y cierre de conexiones
856
+ */
857
+ class DatabaseConnectionManager {
858
+ constructor(configManager) {
859
+ this.configManager = configManager;
860
+ this.db = null;
861
+ this.isAvailable = false;
862
+ }
863
+
864
+ /**
865
+ * Inicializa la conexión con IndexedDB
866
+ * @returns {Promise<Object>} Resultado de la inicialización
867
+ */
868
+ async init() {
869
+ const initResult = {
870
+ success: false,
871
+ error: null,
872
+ timestamp: new Date().toISOString()
873
+ };
874
+
875
+ try {
876
+ // Validar configuración
877
+ const configValidation = this.configManager.validateConfig();
878
+ if (!configValidation.isValid) {
879
+ initResult.error = `Configuración inválida: ${configValidation.errors.join(', ')}`;
880
+ return initResult;
881
+ }
882
+
883
+ // Verificar disponibilidad de IndexedDB
884
+ const availabilityCheck = this.configManager.checkIndexedDBAvailability();
885
+ if (!availabilityCheck.isAvailable) {
886
+ initResult.error = availabilityCheck.reason;
887
+ return initResult;
888
+ }
889
+
890
+ // Abrir conexión
891
+ const connectionResult = await this.openConnection();
892
+ if (!connectionResult.success) {
893
+ initResult.error = connectionResult.error;
894
+ return initResult;
895
+ }
896
+
897
+ this.db = connectionResult.db;
898
+ this.isAvailable = true;
899
+ initResult.success = true;
900
+
901
+ return initResult;
902
+ } catch (error) {
903
+ initResult.error = `Error inesperado: ${error.message}`;
904
+ return initResult;
905
+ }
906
+ }
907
+
908
+ /**
909
+ * Abre la conexión con IndexedDB
910
+ * @returns {Promise<Object>} Resultado de la conexión
911
+ */
912
+ openConnection() {
913
+ return new Promise((resolve) => {
914
+ const config = this.configManager.getConfig();
915
+ const request = indexedDB.open(config.dbName, config.dbVersion);
916
+
917
+ request.onerror = () => {
918
+ resolve({
919
+ success: false,
920
+ error: 'Error abriendo IndexedDB',
921
+ db: null
922
+ });
923
+ };
924
+
925
+ request.onupgradeneeded = (event) => {
926
+ const db = event.target.result;
927
+ const storeConfig = this.configManager.getStoreConfig();
928
+
929
+ if (!db.objectStoreNames.contains(config.storeName)) {
930
+ db.createObjectStore(config.storeName, storeConfig);
931
+ }
932
+ };
933
+
934
+ request.onsuccess = () => {
935
+ resolve({
936
+ success: true,
937
+ error: null,
938
+ db: request.result
939
+ });
940
+ };
941
+ });
942
+ }
943
+
944
+ /**
945
+ * Cierra la conexión con la base de datos
946
+ * @returns {Object} Resultado del cierre
947
+ */
948
+ close() {
949
+ const closeResult = {
950
+ success: false,
951
+ error: null,
952
+ timestamp: new Date().toISOString()
953
+ };
954
+
955
+ try {
956
+ if (this.db) {
957
+ this.db.close();
958
+ this.db = null;
959
+ this.isAvailable = false;
960
+ closeResult.success = true;
961
+ } else {
962
+ closeResult.error = 'No hay conexión activa para cerrar';
963
+ }
964
+ } catch (error) {
965
+ closeResult.error = `Error cerrando conexión: ${error.message}`;
966
+ }
967
+
968
+ return closeResult;
969
+ }
970
+
971
+ /**
972
+ * Verifica si la base de datos está disponible
973
+ * @returns {boolean} True si está disponible
974
+ */
975
+ isDatabaseAvailable() {
976
+ return this.isAvailable && this.db !== null;
977
+ }
978
+
979
+ /**
980
+ * Obtiene la instancia de la base de datos
981
+ * @returns {IDBDatabase|null} Instancia de la base de datos
982
+ */
983
+ getDatabase() {
984
+ return this.isDatabaseAvailable() ? this.db : null;
985
+ }
986
+ }
987
+
988
+ /**
989
+ * DatabaseTransactionManager - Maneja las transacciones de IndexedDB
990
+ * Responsabilidad única: Gestionar transacciones de lectura y escritura
991
+ */
992
+ class DatabaseTransactionManager {
993
+ constructor(connectionManager, configManager) {
994
+ this.connectionManager = connectionManager;
995
+ this.configManager = configManager;
996
+ }
997
+
998
+ /**
999
+ * Obtiene una transacción de lectura
1000
+ * @returns {IDBTransaction} Transacción de lectura
1001
+ * @throws {Error} Si la base de datos no está disponible
1002
+ */
1003
+ getReadTransaction() {
1004
+ this.ensureDatabaseAvailable();
1005
+
1006
+ const config = this.configManager.getConfig();
1007
+ const db = this.connectionManager.getDatabase();
1008
+
1009
+ return db.transaction([config.storeName], 'readonly');
1010
+ }
1011
+
1012
+ /**
1013
+ * Obtiene una transacción de escritura
1014
+ * @returns {IDBTransaction} Transacción de escritura
1015
+ * @throws {Error} Si la base de datos no está disponible
1016
+ */
1017
+ getWriteTransaction() {
1018
+ this.ensureDatabaseAvailable();
1019
+
1020
+ const config = this.configManager.getConfig();
1021
+ const db = this.connectionManager.getDatabase();
1022
+
1023
+ return db.transaction([config.storeName], 'readwrite');
1024
+ }
1025
+
1026
+ /**
1027
+ * Obtiene el object store para una transacción
1028
+ * @param {IDBTransaction} transaction - Transacción activa
1029
+ * @returns {IDBObjectStore} Object store
1030
+ */
1031
+ getObjectStore(transaction) {
1032
+ const config = this.configManager.getConfig();
1033
+ return transaction.objectStore(config.storeName);
1034
+ }
1035
+
1036
+ /**
1037
+ * Ejecuta una operación de lectura de manera segura
1038
+ * @param {Function} operation - Operación a ejecutar
1039
+ * @returns {Promise<Object>} Resultado de la operación
1040
+ */
1041
+ async executeReadOperation(operation) {
1042
+ const operationResult = {
1043
+ success: false,
1044
+ data: null,
1045
+ error: null,
1046
+ timestamp: new Date().toISOString()
1047
+ };
1048
+
1049
+ try {
1050
+ const transaction = this.getReadTransaction();
1051
+ const store = this.getObjectStore(transaction);
1052
+
1053
+ const result = await operation(store);
1054
+
1055
+ operationResult.success = true;
1056
+ operationResult.data = result;
1057
+
1058
+ return operationResult;
1059
+ } catch (error) {
1060
+ operationResult.error = `Error en operación de lectura: ${error.message}`;
1061
+ return operationResult;
1062
+ }
1063
+ }
1064
+
1065
+ /**
1066
+ * Ejecuta una operación de escritura de manera segura
1067
+ * @param {Function} operation - Operación a ejecutar
1068
+ * @returns {Promise<Object>} Resultado de la operación
1069
+ */
1070
+ async executeWriteOperation(operation) {
1071
+ const operationResult = {
1072
+ success: false,
1073
+ data: null,
1074
+ error: null,
1075
+ timestamp: new Date().toISOString()
1076
+ };
1077
+
1078
+ try {
1079
+ const transaction = this.getWriteTransaction();
1080
+ const store = this.getObjectStore(transaction);
1081
+
1082
+ const result = await operation(store);
1083
+
1084
+ operationResult.success = true;
1085
+ operationResult.data = result;
1086
+
1087
+ return operationResult;
1088
+ } catch (error) {
1089
+ operationResult.error = `Error en operación de escritura: ${error.message}`;
1090
+ return operationResult;
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * Verifica que la base de datos esté disponible
1096
+ * @throws {Error} Si la base de datos no está disponible
1097
+ */
1098
+ ensureDatabaseAvailable() {
1099
+ if (!this.connectionManager.isDatabaseAvailable()) {
1100
+ throw new Error('Database not available');
1101
+ }
1102
+ }
1103
+
1104
+ /**
1105
+ * Obtiene información sobre el estado de las transacciones
1106
+ * @returns {Object} Estado de las transacciones
1107
+ */
1108
+ getTransactionStatus() {
1109
+ return {
1110
+ isDatabaseAvailable: this.connectionManager.isDatabaseAvailable(),
1111
+ storeName: this.configManager.getConfig().storeName,
1112
+ timestamp: new Date().toISOString()
1113
+ };
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * DatabaseManager - Coordinador de la gestión de IndexedDB
1119
+ * Responsabilidad única: Coordinar los managers especializados
1120
+ */
1121
+ class DatabaseManager {
1122
+ constructor(dbName, dbVersion, storeName) {
1123
+ this.configManager = new DatabaseConfigManager(dbName, dbVersion, storeName);
1124
+ this.connectionManager = new DatabaseConnectionManager(this.configManager);
1125
+ this.transactionManager = new DatabaseTransactionManager(this.connectionManager, this.configManager);
1126
+ }
1127
+
1128
+ /**
1129
+ * Inicializa la conexión con IndexedDB
1130
+ */
1131
+ async init() {
1132
+ const initResult = await this.connectionManager.init();
1133
+
1134
+ if (initResult.success) {
1135
+ console.log('SyntropyFront: Base de datos inicializada');
1136
+ } else {
1137
+ console.warn('SyntropyFront: Error inicializando base de datos:', initResult.error);
1138
+ }
1139
+
1140
+ return initResult.success;
1141
+ }
1142
+
1143
+ /**
1144
+ * Obtiene una transacción de lectura
1145
+ */
1146
+ getReadTransaction() {
1147
+ return this.transactionManager.getReadTransaction();
1148
+ }
1149
+
1150
+ /**
1151
+ * Obtiene una transacción de escritura
1152
+ */
1153
+ getWriteTransaction() {
1154
+ return this.transactionManager.getWriteTransaction();
1155
+ }
1156
+
1157
+ /**
1158
+ * Cierra la conexión con la base de datos
1159
+ */
1160
+ close() {
1161
+ const closeResult = this.connectionManager.close();
1162
+
1163
+ if (!closeResult.success) {
1164
+ console.warn('SyntropyFront: Error cerrando base de datos:', closeResult.error);
1165
+ }
1166
+
1167
+ return closeResult.success;
1168
+ }
1169
+
1170
+ /**
1171
+ * Verifica si la base de datos está disponible
1172
+ */
1173
+ isDatabaseAvailable() {
1174
+ return this.connectionManager.isDatabaseAvailable();
1175
+ }
1176
+
1177
+ // ===== Propiedades de compatibilidad =====
1178
+
1179
+ /**
1180
+ * @deprecated Usar configManager.getConfig().dbName
1181
+ */
1182
+ get dbName() {
1183
+ return this.configManager.dbName;
1184
+ }
1185
+
1186
+ /**
1187
+ * @deprecated Usar configManager.getConfig().dbVersion
1188
+ */
1189
+ get dbVersion() {
1190
+ return this.configManager.dbVersion;
1191
+ }
1192
+
1193
+ /**
1194
+ * @deprecated Usar configManager.getConfig().storeName
1195
+ */
1196
+ get storeName() {
1197
+ return this.configManager.storeName;
1198
+ }
1199
+
1200
+ /**
1201
+ * @deprecated Usar connectionManager.getDatabase()
1202
+ */
1203
+ get db() {
1204
+ return this.connectionManager.getDatabase();
1205
+ }
1206
+
1207
+ /**
1208
+ * @deprecated Usar connectionManager.isDatabaseAvailable()
1209
+ */
1210
+ get isAvailable() {
1211
+ return this.connectionManager.isDatabaseAvailable();
1212
+ }
1213
+ }
1214
+
1215
+ /**
1216
+ * StorageManager - Maneja las operaciones CRUD de IndexedDB
1217
+ * Responsabilidad única: Gestionar operaciones de almacenamiento y recuperación
1218
+ */
1219
+ class StorageManager {
1220
+ constructor(databaseManager, serializationManager) {
1221
+ this.databaseManager = databaseManager;
1222
+ this.serializationManager = serializationManager;
1223
+ }
1224
+
1225
+ /**
1226
+ * Guarda items en el almacenamiento
1227
+ * @param {Array} items - Items a guardar
1228
+ * @returns {Promise<number>} ID del item guardado
1229
+ */
1230
+ async save(items) {
1231
+ this.ensureDatabaseAvailable();
1232
+
1233
+ const serializationResult = this.serializationManager.serialize(items);
1234
+ const serializedData = this.serializationManager.getData(serializationResult, '[]');
1235
+
1236
+ const item = {
1237
+ items: serializedData,
1238
+ timestamp: new Date().toISOString(),
1239
+ retryCount: 0,
1240
+ serializationError: serializationResult.error
1241
+ };
1242
+
1243
+ return this.executeWriteOperation(store => store.add(item));
1244
+ }
1245
+
1246
+ /**
1247
+ * Obtiene todos los items del almacenamiento
1248
+ * @returns {Promise<Array>} Items deserializados
1249
+ */
1250
+ async retrieve() {
1251
+ if (!this.databaseManager.isDatabaseAvailable()) {
1252
+ return [];
1253
+ }
1254
+
1255
+ const rawItems = await this.executeReadOperation(store => store.getAll());
1256
+ return this.deserializeItems(rawItems);
1257
+ }
1258
+
1259
+ /**
1260
+ * Obtiene un item específico por ID
1261
+ * @param {number} id - ID del item
1262
+ * @returns {Promise<Object|null>} Item deserializado o null
1263
+ */
1264
+ async retrieveById(id) {
1265
+ if (!this.databaseManager.isDatabaseAvailable()) {
1266
+ return null;
1267
+ }
1268
+
1269
+ const rawItem = await this.executeReadOperation(store => store.get(id));
1270
+ return rawItem ? this.deserializeItem(rawItem) : null;
1271
+ }
1272
+
1273
+ /**
1274
+ * Remueve un item del almacenamiento
1275
+ * @param {number} id - ID del item a remover
1276
+ * @returns {Promise<void>}
1277
+ */
1278
+ async remove(id) {
1279
+ this.ensureDatabaseAvailable();
1280
+ return this.executeWriteOperation(store => store.delete(id));
1281
+ }
1282
+
1283
+ /**
1284
+ * Actualiza un item en el almacenamiento
1285
+ * @param {number} id - ID del item
1286
+ * @param {Object} updates - Campos a actualizar
1287
+ * @returns {Promise<number>} ID del item actualizado
1288
+ */
1289
+ async update(id, updates) {
1290
+ this.ensureDatabaseAvailable();
1291
+
1292
+ const currentItem = await this.retrieveById(id);
1293
+ if (!currentItem) {
1294
+ throw new Error('Item not found');
1295
+ }
1296
+
1297
+ const updatedItem = { ...currentItem, ...updates };
1298
+ return this.executeWriteOperation(store => store.put(updatedItem));
1299
+ }
1300
+
1301
+ /**
1302
+ * Limpia todo el almacenamiento
1303
+ * @returns {Promise<void>}
1304
+ */
1305
+ async clear() {
1306
+ this.ensureDatabaseAvailable();
1307
+ return this.executeWriteOperation(store => store.clear());
1308
+ }
1309
+
1310
+ // ===== Métodos privados declarativos =====
1311
+
1312
+ /**
1313
+ * Verifica que la base de datos esté disponible
1314
+ * @throws {Error} Si la base de datos no está disponible
1315
+ */
1316
+ ensureDatabaseAvailable() {
1317
+ if (!this.databaseManager.isDatabaseAvailable()) {
1318
+ throw new Error('Database not available');
1319
+ }
1320
+ }
1321
+
1322
+ /**
1323
+ * Ejecuta una operación de lectura de manera declarativa
1324
+ * @param {Function} operation - Operación a ejecutar en el store
1325
+ * @returns {Promise<*>} Resultado de la operación
1326
+ */
1327
+ executeReadOperation(operation) {
1328
+ return new Promise((resolve, reject) => {
1329
+ try {
1330
+ const transaction = this.databaseManager.getReadTransaction();
1331
+ const store = transaction.objectStore(this.databaseManager.storeName);
1332
+ const request = operation(store);
1333
+
1334
+ request.onsuccess = () => resolve(request.result);
1335
+ request.onerror = () => reject(request.error);
1336
+ } catch (error) {
1337
+ reject(error);
1338
+ }
1339
+ });
1340
+ }
1341
+
1342
+ /**
1343
+ * Ejecuta una operación de escritura de manera declarativa
1344
+ * @param {Function} operation - Operación a ejecutar en el store
1345
+ * @returns {Promise<*>} Resultado de la operación
1346
+ */
1347
+ executeWriteOperation(operation) {
1348
+ return new Promise((resolve, reject) => {
1349
+ try {
1350
+ const transaction = this.databaseManager.getWriteTransaction();
1351
+ const store = transaction.objectStore(this.databaseManager.storeName);
1352
+ const request = operation(store);
1353
+
1354
+ request.onsuccess = () => resolve(request.result);
1355
+ request.onerror = () => reject(request.error);
1356
+ } catch (error) {
1357
+ reject(error);
1358
+ }
1359
+ });
1360
+ }
1361
+
1362
+ /**
1363
+ * Deserializa un array de items
1364
+ * @param {Array} rawItems - Items crudos de la base de datos
1365
+ * @returns {Array} Items deserializados
1366
+ */
1367
+ deserializeItems(rawItems) {
1368
+ return rawItems.map(item => this.deserializeItem(item));
1369
+ }
1370
+
1371
+ /**
1372
+ * Deserializa un item individual
1373
+ * @param {Object} rawItem - Item crudo de la base de datos
1374
+ * @returns {Object} Item deserializado
1375
+ */
1376
+ deserializeItem(rawItem) {
1377
+ const deserializationResult = this.serializationManager.deserialize(rawItem.items);
1378
+ const deserializedItems = this.serializationManager.getData(deserializationResult, []);
1379
+
1380
+ return {
1381
+ ...rawItem,
1382
+ items: deserializedItems,
1383
+ deserializationError: deserializationResult.error
1384
+ };
1385
+ }
1386
+ }
1387
+
1388
+ /**
1389
+ * RetryLogicManager - Maneja la lógica de reintentos y limpieza
1390
+ * Responsabilidad única: Gestionar reintentos y limpieza de items fallidos
1391
+ */
1392
+ class RetryLogicManager {
1393
+ constructor(storageManager, configManager) {
1394
+ this.storageManager = storageManager;
1395
+ this.config = configManager;
1396
+ }
1397
+
1398
+ /**
1399
+ * Intenta enviar items fallidos del buffer persistente
1400
+ * @param {Function} sendCallback - Callback para enviar items
1401
+ * @param {Function} removeCallback - Callback para remover items exitosos
1402
+ */
1403
+ async retryFailedItems(sendCallback, removeCallback) {
1404
+ if (!this.storageManager) {
1405
+ console.warn('SyntropyFront: Storage manager no disponible');
1406
+ return;
1407
+ }
1408
+
1409
+ try {
1410
+ const failedItems = await this.storageManager.retrieve();
1411
+
1412
+ for (const item of failedItems) {
1413
+ if (item.retryCount < this.config.maxRetries) {
1414
+ // Deserializar items del buffer
1415
+ let deserializedItems;
1416
+ try {
1417
+ if (typeof item.items === 'string') {
1418
+ deserializedItems = robustSerializer.deserialize(item.items);
1419
+ } else {
1420
+ deserializedItems = item.items;
1421
+ }
1422
+ } catch (error) {
1423
+ console.error('SyntropyFront: Error deserializando items del buffer:', error);
1424
+ await this.removeFailedItem(item.id);
1425
+ continue;
1426
+ }
1427
+
1428
+ if (sendCallback) {
1429
+ try {
1430
+ await sendCallback(deserializedItems, item.retryCount + 1, item.id);
1431
+
1432
+ // Si el envío fue exitoso, remover del buffer
1433
+ if (removeCallback) {
1434
+ await removeCallback(item.id);
1435
+ } else {
1436
+ await this.removeFailedItem(item.id);
1437
+ }
1438
+
1439
+ console.log(`SyntropyFront: Reintento exitoso para item ${item.id}`);
1440
+ } catch (error) {
1441
+ console.warn(`SyntropyFront: Reintento falló para item ${item.id}:`, error);
1442
+
1443
+ // Incrementar contador de reintentos
1444
+ await this.incrementRetryCount(item.id);
1445
+ }
1446
+ }
1447
+ } else {
1448
+ console.warn(`SyntropyFront: Item ${item.id} excedió máximo de reintentos, removiendo del buffer`);
1449
+ await this.removeFailedItem(item.id);
1450
+ }
1451
+ }
1452
+ } catch (error) {
1453
+ console.error('SyntropyFront: Error procesando reintentos:', error);
1454
+ }
1455
+ }
1456
+
1457
+ /**
1458
+ * Incrementa el contador de reintentos de un item
1459
+ * @param {number} id - ID del item
1460
+ */
1461
+ async incrementRetryCount(id) {
1462
+ try {
1463
+ const currentItem = await this.storageManager.retrieveById(id);
1464
+ if (currentItem) {
1465
+ await this.storageManager.update(id, {
1466
+ retryCount: currentItem.retryCount + 1
1467
+ });
1468
+ }
1469
+ } catch (error) {
1470
+ console.error('SyntropyFront: Error incrementando contador de reintentos:', error);
1471
+ }
1472
+ }
1473
+
1474
+ /**
1475
+ * Remueve un item fallido del buffer
1476
+ * @param {number} id - ID del item
1477
+ */
1478
+ async removeFailedItem(id) {
1479
+ try {
1480
+ await this.storageManager.remove(id);
1481
+ } catch (error) {
1482
+ console.error('SyntropyFront: Error removiendo item fallido:', error);
1483
+ }
1484
+ }
1485
+
1486
+ /**
1487
+ * Limpia items que han excedido el máximo de reintentos
1488
+ */
1489
+ async cleanupExpiredItems() {
1490
+ try {
1491
+ const allItems = await this.storageManager.retrieve();
1492
+ const expiredItems = allItems.filter(item => item.retryCount >= this.config.maxRetries);
1493
+
1494
+ for (const item of expiredItems) {
1495
+ await this.removeFailedItem(item.id);
1496
+ console.warn(`SyntropyFront: Item ${item.id} removido por exceder máximo de reintentos`);
1497
+ }
1498
+
1499
+ if (expiredItems.length > 0) {
1500
+ console.log(`SyntropyFront: Limpieza completada, ${expiredItems.length} items removidos`);
1501
+ }
1502
+ } catch (error) {
1503
+ console.error('SyntropyFront: Error en limpieza de items expirados:', error);
1504
+ }
1505
+ }
1506
+
1507
+ /**
1508
+ * Obtiene estadísticas de reintentos
1509
+ */
1510
+ async getRetryStats() {
1511
+ try {
1512
+ const allItems = await this.storageManager.retrieve();
1513
+
1514
+ const stats = {
1515
+ totalItems: allItems.length,
1516
+ itemsByRetryCount: {},
1517
+ averageRetryCount: 0
1518
+ };
1519
+
1520
+ if (allItems.length > 0) {
1521
+ const totalRetries = allItems.reduce((sum, item) => sum + item.retryCount, 0);
1522
+ stats.averageRetryCount = totalRetries / allItems.length;
1523
+
1524
+ allItems.forEach(item => {
1525
+ const retryCount = item.retryCount;
1526
+ stats.itemsByRetryCount[retryCount] = (stats.itemsByRetryCount[retryCount] || 0) + 1;
1527
+ });
1528
+ }
1529
+
1530
+ return stats;
1531
+ } catch (error) {
1532
+ console.error('SyntropyFront: Error obteniendo estadísticas de reintentos:', error);
1533
+ return {
1534
+ totalItems: 0,
1535
+ itemsByRetryCount: {},
1536
+ averageRetryCount: 0
1537
+ };
1538
+ }
1539
+ }
1540
+ }
1541
+
1542
+ /**
1543
+ * SerializationManager - Maneja la serialización y deserialización de datos
1544
+ * Responsabilidad única: Gestionar la transformación de datos para almacenamiento
1545
+ */
1546
+ class SerializationManager {
1547
+ constructor() {
1548
+ this.serializer = robustSerializer;
1549
+ }
1550
+
1551
+ /**
1552
+ * Serializa items con manejo declarativo de errores
1553
+ * @param {Array} items - Items a serializar
1554
+ * @returns {Object} Resultado de serialización
1555
+ */
1556
+ serialize(items) {
1557
+ const serializationResult = {
1558
+ success: false,
1559
+ data: null,
1560
+ error: null,
1561
+ timestamp: new Date().toISOString()
1562
+ };
1563
+
1564
+ try {
1565
+ const serializedData = this.serializer.serialize(items);
1566
+ return {
1567
+ ...serializationResult,
1568
+ success: true,
1569
+ data: serializedData
1570
+ };
1571
+ } catch (error) {
1572
+ return {
1573
+ ...serializationResult,
1574
+ error: this.createSerializationError(error),
1575
+ data: this.createFallbackData(error)
1576
+ };
1577
+ }
1578
+ }
1579
+
1580
+ /**
1581
+ * Deserializa datos con manejo declarativo de errores
1582
+ * @param {string} serializedData - Datos serializados
1583
+ * @returns {Object} Resultado de deserialización
1584
+ */
1585
+ deserialize(serializedData) {
1586
+ const deserializationResult = {
1587
+ success: false,
1588
+ data: null,
1589
+ error: null,
1590
+ timestamp: new Date().toISOString()
1591
+ };
1592
+
1593
+ try {
1594
+ const deserializedData = this.serializer.deserialize(serializedData);
1595
+ return {
1596
+ ...deserializationResult,
1597
+ success: true,
1598
+ data: deserializedData
1599
+ };
1600
+ } catch (error) {
1601
+ return {
1602
+ ...deserializationResult,
1603
+ error: this.createDeserializationError(error),
1604
+ data: []
1605
+ };
1606
+ }
1607
+ }
1608
+
1609
+ /**
1610
+ * Crea un error de serialización estructurado
1611
+ * @param {Error} error - Error original
1612
+ * @returns {Object} Error estructurado
1613
+ */
1614
+ createSerializationError(error) {
1615
+ return {
1616
+ type: 'serialization_error',
1617
+ message: error.message,
1618
+ originalError: error,
1619
+ timestamp: new Date().toISOString()
1620
+ };
1621
+ }
1622
+
1623
+ /**
1624
+ * Crea un error de deserialización estructurado
1625
+ * @param {Error} error - Error original
1626
+ * @returns {Object} Error estructurado
1627
+ */
1628
+ createDeserializationError(error) {
1629
+ return {
1630
+ type: 'deserialization_error',
1631
+ message: error.message,
1632
+ originalError: error,
1633
+ timestamp: new Date().toISOString()
1634
+ };
1635
+ }
1636
+
1637
+ /**
1638
+ * Crea datos de fallback cuando falla la serialización
1639
+ * @param {Error} error - Error que causó el fallback
1640
+ * @returns {string} Datos de fallback serializados
1641
+ */
1642
+ createFallbackData(error) {
1643
+ const fallbackPayload = {
1644
+ __serializationError: true,
1645
+ error: error.message,
1646
+ timestamp: new Date().toISOString(),
1647
+ fallbackData: 'Items no serializables - usando fallback'
1648
+ };
1649
+
1650
+ return JSON.stringify(fallbackPayload);
1651
+ }
1652
+
1653
+ /**
1654
+ * Verifica si un resultado de serialización fue exitoso
1655
+ * @param {Object} result - Resultado de serialización/deserialización
1656
+ * @returns {boolean} True si fue exitoso
1657
+ */
1658
+ isSuccessful(result) {
1659
+ return Boolean(result && result.success === true);
1660
+ }
1661
+
1662
+ /**
1663
+ * Obtiene los datos de un resultado, con fallback
1664
+ * @param {Object} result - Resultado de serialización/deserialización
1665
+ * @param {*} fallback - Valor por defecto si falla
1666
+ * @returns {*} Datos o fallback
1667
+ */
1668
+ getData(result, fallback = null) {
1669
+ return this.isSuccessful(result) ? result.data : fallback;
1670
+ }
1671
+ }
1672
+
1673
+ /**
1674
+ * PersistentBufferManager - Coordinador del buffer persistente
1675
+ * Responsabilidad única: Coordinar los componentes de almacenamiento persistente
1676
+ */
1677
+ class PersistentBufferManager {
1678
+ constructor(configManager) {
1679
+ this.config = configManager;
1680
+ this.usePersistentBuffer = false;
1681
+
1682
+ // Inicializar componentes especializados
1683
+ this.databaseManager = new DatabaseManager(
1684
+ 'SyntropyFrontBuffer',
1685
+ 1,
1686
+ 'failedItems'
1687
+ );
1688
+
1689
+ this.serializationManager = new SerializationManager();
1690
+ this.storageManager = new StorageManager(this.databaseManager, this.serializationManager);
1691
+ this.retryLogicManager = new RetryLogicManager(this.storageManager, this.config);
1692
+
1693
+ // Inicializar buffer persistente si está disponible
1694
+ this.initPersistentBuffer();
1695
+ }
1696
+
1697
+ /**
1698
+ * Inicializa el buffer persistente
1699
+ */
1700
+ async initPersistentBuffer() {
1701
+ try {
1702
+ const success = await this.databaseManager.init();
1703
+ if (success) {
1704
+ this.usePersistentBuffer = this.config.usePersistentBuffer;
1705
+ console.log('SyntropyFront: Buffer persistente inicializado');
1706
+ }
1707
+ } catch (error) {
1708
+ console.warn('SyntropyFront: Error inicializando buffer persistente:', error);
1709
+ }
1710
+ }
1711
+
1712
+ /**
1713
+ * Guarda items fallidos en el buffer persistente
1714
+ * @param {Array} items - Items a guardar
1715
+ */
1716
+ async save(items) {
1717
+ if (!this.usePersistentBuffer) {
1718
+ return;
1719
+ }
1720
+
1721
+ try {
1722
+ await this.storageManager.save(items);
1723
+ console.log('SyntropyFront: Items guardados en buffer persistente');
1724
+ } catch (error) {
1725
+ console.error('SyntropyFront: Error guardando en buffer persistente:', error);
1726
+ }
1727
+ }
1728
+
1729
+ /**
1730
+ * Obtiene items fallidos del buffer persistente
1731
+ */
1732
+ async retrieve() {
1733
+ if (!this.usePersistentBuffer) {
1734
+ return [];
1735
+ }
1736
+
1737
+ try {
1738
+ return await this.storageManager.retrieve();
1739
+ } catch (error) {
1740
+ console.error('SyntropyFront: Error obteniendo del buffer persistente:', error);
1741
+ return [];
1742
+ }
1743
+ }
1744
+
1745
+ /**
1746
+ * Remueve items del buffer persistente
1747
+ * @param {number} id - ID del item a remover
1748
+ */
1749
+ async remove(id) {
1750
+ if (!this.usePersistentBuffer) {
1751
+ return;
1752
+ }
1753
+
1754
+ try {
1755
+ await this.storageManager.remove(id);
1756
+ } catch (error) {
1757
+ console.error('SyntropyFront: Error removiendo del buffer persistente:', error);
1758
+ }
1759
+ }
1760
+
1761
+ /**
1762
+ * Intenta enviar items fallidos del buffer persistente
1763
+ * @param {Function} sendCallback - Callback para enviar items
1764
+ * @param {Function} removeCallback - Callback para remover items exitosos
1765
+ */
1766
+ async retryFailedItems(sendCallback, removeCallback) {
1767
+ if (!this.usePersistentBuffer) {
1768
+ return;
1769
+ }
1770
+
1771
+ await this.retryLogicManager.retryFailedItems(sendCallback, removeCallback);
1772
+ }
1773
+
1774
+ /**
1775
+ * Limpia items que han excedido el máximo de reintentos
1776
+ */
1777
+ async cleanupExpiredItems() {
1778
+ if (!this.usePersistentBuffer) {
1779
+ return;
1780
+ }
1781
+
1782
+ await this.retryLogicManager.cleanupExpiredItems();
1783
+ }
1784
+
1785
+ /**
1786
+ * Obtiene estadísticas del buffer persistente
1787
+ */
1788
+ async getStats() {
1789
+ if (!this.usePersistentBuffer) {
1790
+ return {
1791
+ totalItems: 0,
1792
+ itemsByRetryCount: {},
1793
+ averageRetryCount: 0,
1794
+ isAvailable: false
1795
+ };
1796
+ }
1797
+
1798
+ try {
1799
+ const retryStats = await this.retryLogicManager.getRetryStats();
1800
+ return {
1801
+ ...retryStats,
1802
+ isAvailable: this.isAvailable()
1803
+ };
1804
+ } catch (error) {
1805
+ console.error('SyntropyFront: Error obteniendo estadísticas:', error);
1806
+ return {
1807
+ totalItems: 0,
1808
+ itemsByRetryCount: {},
1809
+ averageRetryCount: 0,
1810
+ isAvailable: this.isAvailable()
1811
+ };
1812
+ }
1813
+ }
1814
+
1815
+ /**
1816
+ * Verifica si el buffer persistente está disponible
1817
+ */
1818
+ isAvailable() {
1819
+ return this.usePersistentBuffer && this.databaseManager.isDatabaseAvailable();
1820
+ }
1821
+
1822
+ /**
1823
+ * Limpia todo el buffer persistente
1824
+ */
1825
+ async clear() {
1826
+ if (!this.usePersistentBuffer) {
1827
+ return;
1828
+ }
1829
+
1830
+ try {
1831
+ await this.storageManager.clear();
1832
+ console.log('SyntropyFront: Buffer persistente limpiado');
1833
+ } catch (error) {
1834
+ console.error('SyntropyFront: Error limpiando buffer persistente:', error);
1835
+ }
1836
+ }
1837
+
1838
+ /**
1839
+ * Cierra la conexión con la base de datos
1840
+ */
1841
+ close() {
1842
+ this.databaseManager.close();
1843
+ this.usePersistentBuffer = false;
1844
+ }
1845
+ }
1846
+
1847
+ /**
1848
+ * Copyright 2024 Syntropysoft
1849
+ *
1850
+ * Licensed under the Apache License, Version 2.0 (the "License");
1851
+ * you may not use this file except in compliance with the License.
1852
+ * You may obtain a copy of the License at
1853
+ *
1854
+ * http://www.apache.org/licenses/LICENSE-2.0
1855
+ *
1856
+ * Unless required by applicable law or agreed to in writing, software
1857
+ * distributed under the License is distributed on an "AS IS" BASIS,
1858
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1859
+ * See the License for the specific language governing permissions and
1860
+ * limitations under the License.
1861
+ */
1862
+
1863
+
1864
+ /**
1865
+ * Agent - Envía datos de trazabilidad al backend
1866
+ * Coordinador que usa componentes especializados para cada responsabilidad
1867
+ */
1868
+ class Agent {
1869
+ constructor() {
1870
+ // Componentes especializados
1871
+ this.config = new ConfigurationManager();
1872
+ this.queue = new QueueManager(this.config);
1873
+ this.retry = new RetryManager(this.config);
1874
+ this.transport = new HttpTransport(this.config);
1875
+ this.buffer = new PersistentBufferManager(this.config);
1876
+
1877
+ // Configurar callbacks para coordinación
1878
+ this.setupCallbacks();
1879
+ }
1880
+
1881
+ /**
1882
+ * Configura callbacks para coordinación entre componentes
1883
+ */
1884
+ setupCallbacks() {
1885
+ // Callback para el QueueManager cuando hace flush
1886
+ this.queue.flushCallback = async (items) => {
1887
+ try {
1888
+ await this.transport.send(items);
1889
+ console.log('SyntropyFront: Datos enviados exitosamente');
1890
+ } catch (error) {
1891
+ console.error('SyntropyFront Agent: Error enviando datos:', error);
1892
+
1893
+ // Agregar a cola de reintentos
1894
+ this.retry.addToRetryQueue(items);
1895
+
1896
+ // Guardar en buffer persistente
1897
+ await this.buffer.save(items);
1898
+ }
1899
+ };
1900
+
1901
+ // Callback para el RetryManager cuando procesa reintentos
1902
+ this.retry.sendCallback = async (items) => {
1903
+ return await this.transport.send(items);
1904
+ };
1905
+
1906
+ this.retry.removePersistentCallback = async (id) => {
1907
+ await this.buffer.remove(id);
1908
+ };
1909
+
1910
+ // Callback para el PersistentBufferManager cuando retry items
1911
+ this.buffer.sendCallback = (items, retryCount, persistentId) => {
1912
+ this.retry.addToRetryQueue(items, retryCount, persistentId);
1913
+ };
1914
+ }
1915
+
1916
+
1917
+
1918
+ /**
1919
+ * Configura el agent
1920
+ * @param {Object} config - Configuración del agent
1921
+ */
1922
+ configure(config) {
1923
+ this.config.configure(config);
1924
+ }
1925
+
1926
+ /**
1927
+ * Envía un error al backend
1928
+ * @param {Object} errorPayload - Payload del error
1929
+ * @param {Object} context - Contexto adicional (opcional)
1930
+ */
1931
+ sendError(errorPayload, context = null) {
1932
+ if (!this.config.isAgentEnabled()) {
1933
+ console.warn('SyntropyFront Agent: No configurado, error no enviado');
1934
+ return;
1935
+ }
1936
+
1937
+ // Agregar contexto si está disponible
1938
+ const payloadWithContext = context ? {
1939
+ ...errorPayload,
1940
+ context
1941
+ } : errorPayload;
1942
+
1943
+ // Aplicar encriptación si está configurada
1944
+ const dataToSend = this.transport.applyEncryption(payloadWithContext);
1945
+
1946
+ this.queue.add({
1947
+ type: 'error',
1948
+ data: dataToSend,
1949
+ timestamp: new Date().toISOString()
1950
+ });
1951
+ }
1952
+
1953
+ /**
1954
+ * Envía breadcrumbs al backend
1955
+ * @param {Array} breadcrumbs - Lista de breadcrumbs
1956
+ */
1957
+ sendBreadcrumbs(breadcrumbs) {
1958
+ // Solo enviar breadcrumbs si está habilitado (batchTimeout configurado)
1959
+ if (!this.config.isAgentEnabled() || !this.config.shouldSendBreadcrumbs() || !breadcrumbs.length) {
1960
+ return;
1961
+ }
1962
+
1963
+ // Aplicar encriptación si está configurada
1964
+ const dataToSend = this.transport.applyEncryption(breadcrumbs);
1965
+
1966
+ this.queue.add({
1967
+ type: 'breadcrumbs',
1968
+ data: dataToSend,
1969
+ timestamp: new Date().toISOString()
1970
+ });
1971
+ }
1972
+
1973
+ /**
1974
+ * Añade un item a la cola de envío (método público para compatibilidad)
1975
+ * @param {Object} item - Item a añadir
1976
+ */
1977
+ addToQueue(item) {
1978
+ this.queue.add(item);
1979
+ }
1980
+
1981
+ /**
1982
+ * Añade items a la cola de reintentos (método público para compatibilidad)
1983
+ * @param {Array} items - Items a reintentar
1984
+ * @param {number} retryCount - Número de reintento
1985
+ * @param {number} persistentId - ID en buffer persistente (opcional)
1986
+ */
1987
+ addToRetryQueue(items, retryCount = 1, persistentId = null) {
1988
+ this.retry.addToRetryQueue(items, retryCount, persistentId);
1989
+ }
1990
+
1991
+ /**
1992
+ * Procesa la cola de reintentos (método público para compatibilidad)
1993
+ */
1994
+ async processRetryQueue() {
1995
+ await this.retry.processRetryQueue(
1996
+ this.retry.sendCallback,
1997
+ this.retry.removePersistentCallback
1998
+ );
1999
+ }
2000
+
2001
+ /**
2002
+ * Envía todos los items en cola
2003
+ */
2004
+ async flush() {
2005
+ await this.queue.flush(this.queue.flushCallback);
2006
+ }
2007
+
2008
+ /**
2009
+ * Fuerza el envío inmediato de todos los datos pendientes
2010
+ */
2011
+ async forceFlush() {
2012
+ await this.flush();
2013
+
2014
+ // También intentar enviar items en cola de reintentos
2015
+ if (!this.retry.isEmpty()) {
2016
+ console.log('SyntropyFront: Intentando enviar items en cola de reintentos...');
2017
+ await this.processRetryQueue();
2018
+ }
2019
+ }
2020
+
2021
+ /**
2022
+ * Obtiene estadísticas del agent
2023
+ * @returns {Object} Estadísticas
2024
+ */
2025
+ getStats() {
2026
+ const config = this.config.getConfig();
2027
+ return {
2028
+ queueLength: this.queue.getSize(),
2029
+ retryQueueLength: this.retry.getSize(),
2030
+ isEnabled: this.config.isAgentEnabled(),
2031
+ usePersistentBuffer: config.usePersistentBuffer,
2032
+ maxRetries: config.maxRetries
2033
+ };
2034
+ }
2035
+
2036
+ /**
2037
+ * Intenta enviar items fallidos del buffer persistente
2038
+ */
2039
+ async retryFailedItems() {
2040
+ await this.buffer.retryFailedItems(this.buffer.sendCallback);
2041
+ }
2042
+
2043
+ /**
2044
+ * Desactiva el agent
2045
+ */
2046
+ disable() {
2047
+ this.config.configure({ endpoint: null }); // Deshabilitar
2048
+ this.queue.clear();
2049
+ this.retry.clear();
2050
+ }
2051
+ }
2052
+
2053
+ // Instancia singleton
2054
+ const agent = new Agent();
2055
+
2056
+ /**
2057
+ * ContextCollector - Recolector dinámico de contexto
2058
+ * Sistema elegante para recolectar datos según lo que pida el usuario
2059
+ * Por defecto: Sets curados y seguros
2060
+ * Configuración específica: El usuario elige exactamente qué quiere
2061
+ */
2062
+ class ContextCollector {
2063
+ constructor() {
2064
+ // Sets curados por defecto (seguros y útiles)
2065
+ this.defaultContexts = {
2066
+ device: {
2067
+ userAgent: () => navigator.userAgent,
2068
+ language: () => navigator.language,
2069
+ screen: () => ({
2070
+ width: window.screen.width,
2071
+ height: window.screen.height
2072
+ }),
2073
+ timezone: () => Intl.DateTimeFormat().resolvedOptions().timeZone
2074
+ },
2075
+ window: {
2076
+ url: () => window.location.href,
2077
+ viewport: () => ({
2078
+ width: window.innerWidth,
2079
+ height: window.innerHeight
2080
+ }),
2081
+ title: () => document.title
2082
+ },
2083
+ session: {
2084
+ sessionId: () => this.generateSessionId(),
2085
+ pageLoadTime: () => performance.now()
2086
+ },
2087
+ ui: {
2088
+ visibility: () => document.visibilityState,
2089
+ activeElement: () => document.activeElement ? {
2090
+ tagName: document.activeElement.tagName
2091
+ } : null
2092
+ },
2093
+ network: {
2094
+ online: () => navigator.onLine,
2095
+ connection: () => navigator.connection ? {
2096
+ effectiveType: navigator.connection.effectiveType
2097
+ } : null
2098
+ }
2099
+ };
2100
+
2101
+ // Mapeo completo de todos los campos disponibles
2102
+ this.allFields = {
2103
+ device: {
2104
+ userAgent: () => navigator.userAgent,
2105
+ language: () => navigator.language,
2106
+ languages: () => navigator.languages,
2107
+ screen: () => ({
2108
+ width: window.screen.width,
2109
+ height: window.screen.height,
2110
+ availWidth: window.screen.availWidth,
2111
+ availHeight: window.screen.availHeight,
2112
+ colorDepth: window.screen.colorDepth,
2113
+ pixelDepth: window.screen.pixelDepth
2114
+ }),
2115
+ timezone: () => Intl.DateTimeFormat().resolvedOptions().timeZone,
2116
+ cookieEnabled: () => navigator.cookieEnabled,
2117
+ doNotTrack: () => navigator.doNotTrack
2118
+ },
2119
+ window: {
2120
+ url: () => window.location.href,
2121
+ pathname: () => window.location.pathname,
2122
+ search: () => window.location.search,
2123
+ hash: () => window.location.hash,
2124
+ referrer: () => document.referrer,
2125
+ title: () => document.title,
2126
+ viewport: () => ({
2127
+ width: window.innerWidth,
2128
+ height: window.innerHeight
2129
+ })
2130
+ },
2131
+ storage: {
2132
+ localStorage: () => {
2133
+ const keys = Object.keys(localStorage);
2134
+ return {
2135
+ keys: keys.length,
2136
+ size: JSON.stringify(localStorage).length,
2137
+ keyNames: keys // Solo nombres, no valores
2138
+ };
2139
+ },
2140
+ sessionStorage: () => {
2141
+ const keys = Object.keys(sessionStorage);
2142
+ return {
2143
+ keys: keys.length,
2144
+ size: JSON.stringify(sessionStorage).length,
2145
+ keyNames: keys // Solo nombres, no valores
2146
+ };
2147
+ }
2148
+ },
2149
+ network: {
2150
+ online: () => navigator.onLine,
2151
+ connection: () => navigator.connection ? {
2152
+ effectiveType: navigator.connection.effectiveType,
2153
+ downlink: navigator.connection.downlink,
2154
+ rtt: navigator.connection.rtt
2155
+ } : null
2156
+ },
2157
+ ui: {
2158
+ focused: () => document.hasFocus(),
2159
+ visibility: () => document.visibilityState,
2160
+ activeElement: () => document.activeElement ? {
2161
+ tagName: document.activeElement.tagName,
2162
+ id: document.activeElement.id,
2163
+ className: document.activeElement.className
2164
+ } : null
2165
+ },
2166
+ performance: {
2167
+ memory: () => window.performance && window.performance.memory ? {
2168
+ used: Math.round(window.performance.memory.usedJSHeapSize / 1048576),
2169
+ total: Math.round(window.performance.memory.totalJSHeapSize / 1048576),
2170
+ limit: Math.round(window.performance.memory.jsHeapSizeLimit / 1048576)
2171
+ } : null,
2172
+ timing: () => window.performance ? {
2173
+ navigationStart: window.performance.timing.navigationStart,
2174
+ loadEventEnd: window.performance.timing.loadEventEnd
2175
+ } : null
2176
+ },
2177
+ session: {
2178
+ sessionId: () => this.generateSessionId(),
2179
+ startTime: () => new Date().toISOString(),
2180
+ pageLoadTime: () => performance.now()
2181
+ }
2182
+ };
2183
+ }
2184
+
2185
+ /**
2186
+ * Recolecta contexto según la configuración
2187
+ * @param {Object} contextConfig - Configuración de contexto
2188
+ * @returns {Object} Contexto recolectado
2189
+ */
2190
+ collect(contextConfig = {}) {
2191
+ const context = {};
2192
+
2193
+ Object.entries(contextConfig).forEach(([contextType, config]) => {
2194
+ try {
2195
+ if (config === true) {
2196
+ // Usar set curado por defecto
2197
+ context[contextType] = this.collectDefaultContext(contextType);
2198
+ } else if (Array.isArray(config)) {
2199
+ // Configuración específica: array de campos
2200
+ context[contextType] = this.collectSpecificFields(contextType, config);
2201
+ } else if (config === false) {
2202
+ // Explícitamente deshabilitado
2203
+ // No hacer nada
2204
+ } else {
2205
+ console.warn(`SyntropyFront: Configuración de contexto inválida para ${contextType}:`, config);
2206
+ }
2207
+ } catch (error) {
2208
+ console.warn(`SyntropyFront: Error recolectando contexto ${contextType}:`, error);
2209
+ context[contextType] = { error: 'Failed to collect' };
2210
+ }
2211
+ });
2212
+
2213
+ return context;
2214
+ }
2215
+
2216
+ /**
2217
+ * Recolecta el set curado por defecto
2218
+ * @param {string} contextType - Tipo de contexto
2219
+ * @returns {Object} Contexto por defecto
2220
+ */
2221
+ collectDefaultContext(contextType) {
2222
+ const defaultContext = this.defaultContexts[contextType];
2223
+ if (!defaultContext) {
2224
+ console.warn(`SyntropyFront: No hay set por defecto para ${contextType}`);
2225
+ return {};
2226
+ }
2227
+
2228
+ const result = {};
2229
+ Object.entries(defaultContext).forEach(([field, getter]) => {
2230
+ try {
2231
+ result[field] = getter();
2232
+ } catch (error) {
2233
+ console.warn(`SyntropyFront: Error recolectando campo ${field} de ${contextType}:`, error);
2234
+ result[field] = null;
2235
+ }
2236
+ });
2237
+
2238
+ return result;
2239
+ }
2240
+
2241
+ /**
2242
+ * Recolecta campos específicos
2243
+ * @param {string} contextType - Tipo de contexto
2244
+ * @param {Array} fields - Campos específicos a recolectar
2245
+ * @returns {Object} Contexto específico
2246
+ */
2247
+ collectSpecificFields(contextType, fields) {
2248
+ const allFields = this.allFields[contextType];
2249
+ if (!allFields) {
2250
+ console.warn(`SyntropyFront: Tipo de contexto desconocido: ${contextType}`);
2251
+ return {};
2252
+ }
2253
+
2254
+ const result = {};
2255
+ fields.forEach(field => {
2256
+ try {
2257
+ if (allFields[field]) {
2258
+ result[field] = allFields[field]();
2259
+ } else {
2260
+ console.warn(`SyntropyFront: Campo ${field} no disponible en ${contextType}`);
2261
+ }
2262
+ } catch (error) {
2263
+ console.warn(`SyntropyFront: Error recolectando campo ${field} de ${contextType}:`, error);
2264
+ result[field] = null;
2265
+ }
2266
+ });
2267
+
2268
+ return result;
2269
+ }
2270
+
2271
+ /**
2272
+ * Genera un ID de sesión seguro usando crypto.randomUUID() cuando esté disponible
2273
+ * @returns {string} ID de sesión seguro
2274
+ */
2275
+ generateSecureId() {
2276
+ try {
2277
+ // Intentar usar crypto.randomUUID() si está disponible (Node.js 14.17+, browsers modernos)
2278
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
2279
+ return crypto.randomUUID();
2280
+ }
2281
+
2282
+ // Fallback para navegadores más antiguos: usar crypto.getRandomValues()
2283
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
2284
+ const array = new Uint8Array(16);
2285
+ crypto.getRandomValues(array);
2286
+
2287
+ // Convertir a formato UUID v4
2288
+ const hex = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
2289
+ return [
2290
+ hex.slice(0, 8),
2291
+ hex.slice(8, 12),
2292
+ hex.slice(12, 16),
2293
+ hex.slice(16, 20),
2294
+ hex.slice(20, 32)
2295
+ ].join('-');
2296
+ }
2297
+
2298
+ // Fallback final: timestamp + random (menos seguro pero funcional)
2299
+ const timestamp = Date.now().toString(36);
2300
+ const random = Math.random().toString(36).substring(2, 15);
2301
+ return `${timestamp}-${random}`;
2302
+ } catch (error) {
2303
+ console.warn('SyntropyFront: Error generando ID seguro, usando fallback:', error);
2304
+ // Fallback de emergencia
2305
+ return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2306
+ }
2307
+ }
2308
+
2309
+ /**
2310
+ * Genera un ID de sesión simple
2311
+ */
2312
+ generateSessionId() {
2313
+ if (!this._sessionId) {
2314
+ this._sessionId = `session_${this.generateSecureId()}`;
2315
+ }
2316
+ return this._sessionId;
2317
+ }
2318
+
2319
+ /**
2320
+ * Obtiene la lista de tipos de contexto disponibles
2321
+ * @returns {Array} Tipos disponibles
2322
+ */
2323
+ getAvailableTypes() {
2324
+ return Object.keys(this.allFields);
2325
+ }
2326
+
2327
+ /**
2328
+ * Obtiene la lista de campos disponibles para un tipo de contexto
2329
+ * @param {string} contextType - Tipo de contexto
2330
+ * @returns {Array} Campos disponibles
2331
+ */
2332
+ getAvailableFields(contextType) {
2333
+ const fields = this.allFields[contextType];
2334
+ return fields ? Object.keys(fields) : [];
2335
+ }
2336
+
2337
+ /**
2338
+ * Obtiene información sobre los sets por defecto
2339
+ * @returns {Object} Información de sets por defecto
2340
+ */
2341
+ getDefaultContextsInfo() {
2342
+ const info = {};
2343
+ Object.entries(this.defaultContexts).forEach(([type, fields]) => {
2344
+ info[type] = Object.keys(fields);
2345
+ });
2346
+ return info;
2347
+ }
2348
+ }
2349
+
2350
+ // Instancia singleton
2351
+ const contextCollector = new ContextCollector();
2352
+
2353
+ /**
2354
+ * Copyright 2024 Syntropysoft
2355
+ *
2356
+ * Licensed under the Apache License, Version 2.0 (the "License");
2357
+ * you may not use this file except in compliance with the License.
2358
+ * You may obtain a copy of the License at
2359
+ *
2360
+ * http://www.apache.org/licenses/LICENSE-2.0
2361
+ *
2362
+ * Unless required by applicable law or agreed to in writing, software
2363
+ * distributed under the License is distributed on an "AS IS" BASIS,
2364
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2365
+ * See the License for the specific language governing permissions and
2366
+ * limitations under the License.
2367
+ */
2368
+
2369
+
2370
+ /**
2371
+ * Interceptors - Observadores que capturan eventos automáticamente
2372
+ * Implementa Chaining Pattern para coexistir con otros APMs
2373
+ */
2374
+ class Interceptors {
2375
+ constructor() {
2376
+ this.isInitialized = false;
2377
+ this.config = {
2378
+ captureClicks: true,
2379
+ captureFetch: true,
2380
+ captureErrors: true,
2381
+ captureUnhandledRejections: true
2382
+ };
2383
+ this.contextTypes = [];
2384
+
2385
+ // Referencias originales para restaurar en destroy()
2386
+ this.originalHandlers = {
2387
+ fetch: null,
2388
+ onerror: null,
2389
+ onunhandledrejection: null
2390
+ };
2391
+
2392
+ // Event listeners para limpiar
2393
+ this.eventListeners = new Map();
2394
+ }
2395
+
2396
+ /**
2397
+ * Configura los interceptores
2398
+ * @param {Object} config - Configuración de interceptores
2399
+ */
2400
+ configure(config) {
2401
+ this.config = { ...this.config, ...config };
2402
+ this.contextTypes = config.context || [];
2403
+ }
2404
+
2405
+ /**
2406
+ * Inicializa todos los interceptores
2407
+ */
2408
+ init() {
2409
+ if (this.isInitialized) {
2410
+ console.warn('SyntropyFront: Interceptors ya están inicializados');
2411
+ return;
2412
+ }
2413
+
2414
+ if (this.config.captureClicks) {
2415
+ this.setupClickInterceptor();
2416
+ }
2417
+
2418
+ if (this.config.captureFetch) {
2419
+ this.setupFetchInterceptor();
2420
+ }
2421
+
2422
+ if (this.config.captureErrors || this.config.captureUnhandledRejections) {
2423
+ this.setupErrorInterceptors();
2424
+ }
2425
+
2426
+ this.isInitialized = true;
2427
+ console.log('SyntropyFront: Interceptors inicializados con Chaining Pattern');
2428
+ }
2429
+
2430
+ /**
2431
+ * Intercepta clics de usuario
2432
+ */
2433
+ setupClickInterceptor() {
2434
+ // Solo configurar en el browser
2435
+ if (typeof document === 'undefined') {
2436
+ console.log('SyntropyFront: Click interceptor no disponible (no browser)');
2437
+ return;
2438
+ }
2439
+
2440
+ let lastClickTime = 0;
2441
+ const THROTTLE_MS = 500;
2442
+
2443
+ const clickHandler = (event) => {
2444
+ const el = event.target;
2445
+ if (!el) return;
2446
+
2447
+ // ✅ THROTTLE: Evitar ráfagas de clicks (ej: double clicks accidentales)
2448
+ const now = Date.now();
2449
+ if (now - lastClickTime < THROTTLE_MS) return;
2450
+ lastClickTime = now;
2451
+
2452
+ // ✅ FILTER: Solo capturar elementos potencialmente interactivos
2453
+ const isInteractive = (element) => {
2454
+ if (!element || element.nodeType !== 1) return false;
2455
+ const interactiveTags = ['a', 'button', 'input', 'select', 'textarea', 'label', 'summary'];
2456
+ const isClickableRole = ['button', 'link', 'checkbox', 'radio', 'menuitem'].includes(element.getAttribute?.('role'));
2457
+
2458
+ let hasPointerCursor = false;
2459
+ try {
2460
+ hasPointerCursor = window.getComputedStyle?.(element)?.cursor === 'pointer';
2461
+ } catch (e) {
2462
+ // Ignorar errores en entornos donde getComputedStyle falla (ej: JSDOM con mocks incompletos)
2463
+ }
2464
+
2465
+ return interactiveTags.includes(element.tagName.toLowerCase()) || isClickableRole || hasPointerCursor;
2466
+ };
2467
+
2468
+ // Si el elemento no es interactivo, buscar hacia arriba en el DOM (bubbling)
2469
+ let target = el;
2470
+ while (target && target !== document.body) {
2471
+ if (isInteractive(target)) break;
2472
+ target = target.parentElement;
2473
+ }
2474
+
2475
+ // Si no encontramos un elemento interactivo, ignoramos el click (reduce ruido)
2476
+ if (!target || target === document.body) return;
2477
+
2478
+ // Genera un selector CSS simple para identificar el elemento
2479
+ let selector = target.tagName.toLowerCase();
2480
+ if (target.id) {
2481
+ selector += `#${target.id}`;
2482
+ } else if (target.className && typeof target.className === 'string') {
2483
+ selector += `.${target.className.split(' ').filter(Boolean).join('.')}`;
2484
+ }
2485
+
2486
+ breadcrumbStore.add({
2487
+ category: 'ui',
2488
+ message: `Usuario hizo click en '${selector}'`,
2489
+ data: {
2490
+ selector,
2491
+ tagName: target.tagName,
2492
+ id: target.id,
2493
+ className: target.className,
2494
+ text: target.innerText?.substring(0, 30).trim() || target.value?.substring(0, 30)
2495
+ }
2496
+ });
2497
+ };
2498
+
2499
+ // Guardar referencia para limpiar después
2500
+ this.eventListeners.set('click', clickHandler);
2501
+ document.addEventListener('click', clickHandler, true);
2502
+ }
2503
+
2504
+ /**
2505
+ * Intercepta llamadas de red (fetch) con Chaining
2506
+ */
2507
+ setupFetchInterceptor() {
2508
+ // Solo configurar en el browser
2509
+ if (typeof window === 'undefined' || !window.fetch) {
2510
+ console.log('SyntropyFront: Fetch interceptor no disponible (no browser/fetch)');
2511
+ return;
2512
+ }
2513
+
2514
+ // Guardar referencia original
2515
+ this.originalHandlers.fetch = window.fetch;
2516
+
2517
+ // Crear nuevo handler que encadena con el original
2518
+ const syntropyFetchHandler = (...args) => {
2519
+ const url = args[0] instanceof Request ? args[0].url : args[0];
2520
+ const method = args[0] instanceof Request ? args[0].method : (args[1]?.method || 'GET');
2521
+
2522
+ breadcrumbStore.add({
2523
+ category: 'network',
2524
+ message: `Request: ${method} ${url}`,
2525
+ data: {
2526
+ url,
2527
+ method,
2528
+ timestamp: Date.now()
2529
+ }
2530
+ });
2531
+
2532
+ // ✅ CHAINING: Llamar al handler original
2533
+ return this.originalHandlers.fetch.apply(window, args);
2534
+ };
2535
+
2536
+ // Sobrescribir con el nuevo handler
2537
+ window.fetch = syntropyFetchHandler;
2538
+ }
2539
+
2540
+ /**
2541
+ * Intercepta errores globales con Chaining
2542
+ */
2543
+ setupErrorInterceptors() {
2544
+ // Solo configurar en el browser
2545
+ if (typeof window === 'undefined') {
2546
+ console.log('SyntropyFront: Error interceptors no disponibles (no browser)');
2547
+ return;
2548
+ }
2549
+
2550
+ if (this.config.captureErrors) {
2551
+ // Guardar referencia original
2552
+ this.originalHandlers.onerror = window.onerror;
2553
+
2554
+ // Crear nuevo handler que encadena con el original
2555
+ const syntropyErrorHandler = (message, source, lineno, colno, error) => {
2556
+ const errorPayload = {
2557
+ type: 'uncaught_exception',
2558
+ error: {
2559
+ message,
2560
+ source,
2561
+ lineno,
2562
+ colno,
2563
+ stack: error?.stack
2564
+ },
2565
+ breadcrumbs: breadcrumbStore.getAll(),
2566
+ timestamp: new Date().toISOString()
2567
+ };
2568
+
2569
+ this.handleError(errorPayload);
2570
+
2571
+ // ✅ CHAINING: Llamar al handler original si existe
2572
+ if (this.originalHandlers.onerror) {
2573
+ try {
2574
+ return this.originalHandlers.onerror(message, source, lineno, colno, error);
2575
+ } catch (originalError) {
2576
+ console.warn('SyntropyFront: Error en handler original:', originalError);
2577
+ return false;
2578
+ }
2579
+ }
2580
+
2581
+ return false; // No prevenir el error por defecto
2582
+ };
2583
+
2584
+ // Sobrescribir con el nuevo handler
2585
+ window.onerror = syntropyErrorHandler;
2586
+ }
2587
+
2588
+ if (this.config.captureUnhandledRejections) {
2589
+ // Guardar referencia original
2590
+ this.originalHandlers.onunhandledrejection = window.onunhandledrejection;
2591
+
2592
+ // Crear nuevo handler que encadena con el original
2593
+ const syntropyRejectionHandler = (event) => {
2594
+ const errorPayload = {
2595
+ type: 'unhandled_rejection',
2596
+ error: {
2597
+ message: event.reason?.message || 'Rechazo de promesa sin mensaje',
2598
+ stack: event.reason?.stack,
2599
+ },
2600
+ breadcrumbs: breadcrumbStore.getAll(),
2601
+ timestamp: new Date().toISOString()
2602
+ };
2603
+
2604
+ this.handleError(errorPayload);
2605
+
2606
+ // ✅ CHAINING: Llamar al handler original si existe
2607
+ if (this.originalHandlers.onunhandledrejection) {
2608
+ try {
2609
+ this.originalHandlers.onunhandledrejection(event);
2610
+ } catch (originalError) {
2611
+ console.warn('SyntropyFront: Error en handler original de rejection:', originalError);
2612
+ }
2613
+ }
2614
+ };
2615
+
2616
+ // Sobrescribir con el nuevo handler
2617
+ window.onunhandledrejection = syntropyRejectionHandler;
2618
+ }
2619
+ }
2620
+
2621
+ /**
2622
+ * Maneja los errores capturados
2623
+ * @param {Object} errorPayload - Payload del error
2624
+ */
2625
+ handleError(errorPayload) {
2626
+ // Recolectar contexto si está configurado
2627
+ const context = this.contextTypes.length > 0 ? contextCollector.collect(this.contextTypes) : null;
2628
+
2629
+ // Enviar al agent si está configurado
2630
+ agent.sendError(errorPayload, context);
2631
+
2632
+ // Callback para manejo personalizado de errores
2633
+ if (this.onError) {
2634
+ this.onError(errorPayload);
2635
+ } else {
2636
+ // Comportamiento por defecto: log a consola
2637
+ console.error('SyntropyFront - Error detectado:', errorPayload);
2638
+ }
2639
+ }
2640
+
2641
+ /**
2642
+ * Desactiva todos los interceptores y restaura handlers originales
2643
+ */
2644
+ destroy() {
2645
+ if (!this.isInitialized) return;
2646
+
2647
+ console.log('SyntropyFront: Limpiando interceptores...');
2648
+
2649
+ // ✅ RESTAURAR: Handlers originales
2650
+ if (this.originalHandlers.fetch) {
2651
+ window.fetch = this.originalHandlers.fetch;
2652
+ console.log('SyntropyFront: fetch original restaurado');
2653
+ }
2654
+
2655
+ if (this.originalHandlers.onerror) {
2656
+ window.onerror = this.originalHandlers.onerror;
2657
+ console.log('SyntropyFront: onerror original restaurado');
2658
+ }
2659
+
2660
+ if (this.originalHandlers.onunhandledrejection) {
2661
+ window.onunhandledrejection = this.originalHandlers.onunhandledrejection;
2662
+ console.log('SyntropyFront: onunhandledrejection original restaurado');
2663
+ }
2664
+
2665
+ // ✅ LIMPIAR: Event listeners
2666
+ if (typeof document !== 'undefined') {
2667
+ this.eventListeners.forEach((handler, eventType) => {
2668
+ document.removeEventListener(eventType, handler, true);
2669
+ console.log(`SyntropyFront: Event listener ${eventType} removido`);
2670
+ });
2671
+ }
2672
+
2673
+ // Limpiar referencias
2674
+ this.originalHandlers = {
2675
+ fetch: null,
2676
+ onerror: null,
2677
+ onunhandledrejection: null
2678
+ };
2679
+ this.eventListeners.clear();
2680
+ this.isInitialized = false;
2681
+
2682
+ console.log('SyntropyFront: Interceptors destruidos y handlers restaurados');
2683
+ }
2684
+
2685
+ /**
2686
+ * Obtiene información sobre los handlers originales
2687
+ * @returns {Object} Información de handlers
2688
+ */
2689
+ getHandlerInfo() {
2690
+ return {
2691
+ isInitialized: this.isInitialized,
2692
+ hasOriginalFetch: !!this.originalHandlers.fetch,
2693
+ hasOriginalOnError: !!this.originalHandlers.onerror,
2694
+ hasOriginalOnUnhandledRejection: !!this.originalHandlers.onunhandledrejection,
2695
+ eventListenersCount: this.eventListeners.size
2696
+ };
2697
+ }
2698
+ }
2699
+
2700
+ // Instancia singleton
2701
+ const interceptors = new Interceptors();
2702
+
2703
+ /**
2704
+ * Copyright 2024 Syntropysoft
2705
+ *
2706
+ * Licensed under the Apache License, Version 2.0 (the "License");
2707
+ * you may not use this file except in compliance with the License.
2708
+ * You may obtain a copy of the License at
2709
+ *
2710
+ * http://www.apache.org/licenses/LICENSE-2.0
2711
+ *
2712
+ * Unless required by applicable law or agreed to in writing, software
2713
+ * distributed under the License is distributed on an "AS IS" BASIS,
2714
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2715
+ * See the License for the specific language governing permissions and
2716
+ * limitations under the License.
2717
+ */
2718
+
2719
+
2720
+ class SyntropyFront {
2721
+ constructor() {
2722
+ this.isActive = false;
2723
+ this.config = {
2724
+ maxEvents: 50,
2725
+ endpoint: null,
2726
+ headers: {},
2727
+ usePersistentBuffer: true,
2728
+ captureClicks: true,
2729
+ captureFetch: true,
2730
+ captureErrors: true,
2731
+ captureUnhandledRejections: true,
2732
+ onError: null
2733
+ };
2734
+
2735
+ // Auto-inicializar
2736
+ this.init();
2737
+ }
2738
+
2739
+ /**
2740
+ * Inicializa la biblioteca y activa los interceptores
2741
+ */
2742
+ init() {
2743
+ if (this.isActive) return;
2744
+
2745
+ // Configurar el agent por defecto
2746
+ agent.configure({
2747
+ endpoint: this.config.endpoint,
2748
+ headers: this.config.headers,
2749
+ usePersistentBuffer: this.config.usePersistentBuffer
2750
+ });
2751
+
2752
+ // Inicializar interceptores
2753
+ interceptors.configure({
2754
+ captureClicks: this.config.captureClicks,
2755
+ captureFetch: this.config.captureFetch,
2756
+ captureErrors: this.config.captureErrors,
2757
+ captureUnhandledRejections: this.config.captureUnhandledRejections
2758
+ });
2759
+
2760
+ // Inyectar callback de error si existe
2761
+ if (this.config.onError) {
2762
+ interceptors.onError = this.config.onError;
2763
+ }
2764
+
2765
+ interceptors.init();
2766
+
2767
+ // Intentar reintentar items fallidos de sesiones previas
2768
+ agent.retryFailedItems().catch(err => {
2769
+ console.warn('SyntropyFront: Error al intentar recuperar items persistentes:', err);
2770
+ });
2771
+
2772
+ this.isActive = true;
2773
+ console.log('🚀 SyntropyFront: Inicializado con arquitectura modular resiliente');
2774
+ }
2775
+
2776
+ /**
2777
+ * Configura SyntropyFront
2778
+ * @param {Object} config - Configuración
2779
+ */
2780
+ configure(config = {}) {
2781
+ // Actualizar configuración local
2782
+ this.config = { ...this.config, ...config };
2783
+
2784
+ // Si se pasa 'fetch', extraer endpoint y headers por compatibilidad
2785
+ if (config.fetch) {
2786
+ this.config.endpoint = config.fetch.url;
2787
+ this.config.headers = config.fetch.options?.headers || {};
2788
+ }
2789
+
2790
+ // Re-configurar componentes internos
2791
+ agent.configure({
2792
+ endpoint: this.config.endpoint,
2793
+ headers: this.config.headers,
2794
+ usePersistentBuffer: this.config.usePersistentBuffer
2795
+ });
2796
+
2797
+ interceptors.configure({
2798
+ captureClicks: this.config.captureClicks,
2799
+ captureFetch: this.config.captureFetch,
2800
+ captureErrors: this.config.captureErrors,
2801
+ captureUnhandledRejections: this.config.captureUnhandledRejections
2802
+ });
2803
+
2804
+ if (this.config.onError) {
2805
+ interceptors.onError = this.config.onError;
2806
+ }
2807
+
2808
+ const mode = this.config.endpoint ? `endpoint: ${this.config.endpoint}` : 'console only';
2809
+ console.log(`✅ SyntropyFront: Configurado - ${mode}`);
2810
+ }
2811
+
2812
+ /**
2813
+ * Añade un breadcrumb manualmente
2814
+ */
2815
+ addBreadcrumb(category, message, data = {}) {
2816
+ return breadcrumbStore.add({ category, message, data });
2817
+ }
2818
+
2819
+ /**
2820
+ * Obtiene todos los breadcrumbs
2821
+ */
2822
+ getBreadcrumbs() {
2823
+ return breadcrumbStore.getAll();
2824
+ }
2825
+
2826
+ /**
2827
+ * Limpia los breadcrumbs
2828
+ */
2829
+ clearBreadcrumbs() {
2830
+ breadcrumbStore.clear();
2831
+ }
2832
+
2833
+ /**
2834
+ * Envía un error manualmente con contexto
2835
+ */
2836
+ sendError(error, context = {}) {
2837
+ const errorPayload = {
2838
+ type: 'manual_error',
2839
+ error: {
2840
+ message: error.message || String(error),
2841
+ name: error.name || 'Error',
2842
+ stack: error.stack
2843
+ },
2844
+ breadcrumbs: this.getBreadcrumbs(),
2845
+ timestamp: new Date().toISOString()
2846
+ };
2847
+
2848
+ agent.sendError(errorPayload, context);
2849
+ return errorPayload;
2850
+ }
2851
+
2852
+ /**
2853
+ * Fuerza el envío de datos pendientes
2854
+ */
2855
+ async flush() {
2856
+ await agent.forceFlush();
2857
+ }
2858
+
2859
+ /**
2860
+ * Obtiene estadísticas de uso
2861
+ */
2862
+ getStats() {
411
2863
  return {
412
- breadcrumbs: this.breadcrumbManager.getCount(),
413
- errors: this.errorManager.getCount(),
414
2864
  isActive: this.isActive,
415
- maxEvents: this.maxEvents,
416
- hasFetchConfig: !!this.fetchConfig,
417
- hasErrorCallback: !!this.onErrorCallback,
418
- endpoint: this.fetchConfig?.url || 'console'
2865
+ breadcrumbs: breadcrumbStore.count(),
2866
+ agent: agent.getStats(),
2867
+ config: { ...this.config }
419
2868
  };
420
2869
  }
2870
+
2871
+ /**
2872
+ * Desactiva la biblioteca y restaura hooks originales
2873
+ */
2874
+ destroy() {
2875
+ interceptors.destroy();
2876
+ agent.disable();
2877
+ this.isActive = false;
2878
+ console.log('SyntropyFront: Desactivado');
2879
+ }
421
2880
  }
422
2881
 
423
- // Single instance - auto-initializes
2882
+ // Instancia única (Singleton)
424
2883
  const syntropyFront = new SyntropyFront();
425
2884
 
426
2885
  export { syntropyFront as default };