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