@syntropysoft/syntropyfront 0.1.0-alpha.1

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 ADDED
@@ -0,0 +1,3357 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /**
6
+ * BreadcrumbStore - Almacén de huellas del usuario
7
+ * Mantiene un historial de las últimas acciones del usuario
8
+ */
9
+ class BreadcrumbStore {
10
+ constructor(maxBreadcrumbs = 25) {
11
+ this.maxBreadcrumbs = maxBreadcrumbs;
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;
43
+ }
44
+
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) {
53
+ const breadcrumb = {
54
+ ...crumb,
55
+ timestamp: new Date().toISOString(),
56
+ };
57
+
58
+ if (this.breadcrumbs.length >= this.maxBreadcrumbs) {
59
+ this.breadcrumbs.shift(); // Elimina el más antiguo
60
+ }
61
+
62
+ this.breadcrumbs.push(breadcrumb);
63
+
64
+ // Callback opcional para logging
65
+ if (this.onBreadcrumbAdded) {
66
+ this.onBreadcrumbAdded(breadcrumb);
67
+ }
68
+
69
+ // Enviar al agent si está configurado
70
+ if (this.agent) {
71
+ this.agent.sendBreadcrumbs([breadcrumb]);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Devuelve todos los breadcrumbs
77
+ * @returns {Array} Copia de todos los breadcrumbs
78
+ */
79
+ getAll() {
80
+ return [...this.breadcrumbs];
81
+ }
82
+
83
+ /**
84
+ * Limpia todos los breadcrumbs
85
+ */
86
+ clear() {
87
+ this.breadcrumbs = [];
88
+ }
89
+
90
+ /**
91
+ * Obtiene breadcrumbs por categoría
92
+ * @param {string} category - Categoría a filtrar
93
+ * @returns {Array} Breadcrumbs de la categoría especificada
94
+ */
95
+ getByCategory(category) {
96
+ return this.breadcrumbs.filter(b => b.category === category);
97
+ }
98
+ }
99
+
100
+ // Instancia singleton
101
+ const breadcrumbStore = new BreadcrumbStore();
102
+
103
+ /**
104
+ * RobustSerializer - Serializador robusto que maneja referencias circulares
105
+ * Implementa una solución similar a flatted pero sin dependencias externas
106
+ */
107
+ class RobustSerializer {
108
+ constructor() {
109
+ this.seen = new WeakSet();
110
+ this.circularRefs = new Map();
111
+ this.refCounter = 0;
112
+ }
113
+
114
+ /**
115
+ * Serializa un objeto de forma segura, manejando referencias circulares
116
+ * @param {any} obj - Objeto a serializar
117
+ * @returns {string} JSON string seguro
118
+ */
119
+ serialize(obj) {
120
+ try {
121
+ // Reset state
122
+ this.seen = new WeakSet();
123
+ this.circularRefs = new Map();
124
+ this.refCounter = 0;
125
+
126
+ // Serializar con manejo de referencias circulares
127
+ const safeObj = this.makeSerializable(obj);
128
+
129
+ // Convertir a JSON
130
+ return JSON.stringify(safeObj);
131
+ } catch (error) {
132
+ console.error('SyntropyFront: Error en serialización robusta:', error);
133
+
134
+ // Fallback: intentar serialización básica con información de error
135
+ return JSON.stringify({
136
+ __serializationError: true,
137
+ error: error.message,
138
+ originalType: typeof obj,
139
+ isObject: obj !== null && typeof obj === 'object',
140
+ timestamp: new Date().toISOString()
141
+ });
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Hace un objeto serializable, manejando referencias circulares
147
+ * @param {any} obj - Objeto a procesar
148
+ * @param {string} path - Ruta actual en el objeto
149
+ * @returns {any} Objeto serializable
150
+ */
151
+ makeSerializable(obj, path = '') {
152
+ // Casos primitivos
153
+ if (obj === null || obj === undefined) {
154
+ return obj;
155
+ }
156
+
157
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
158
+ return obj;
159
+ }
160
+
161
+ // Casos especiales
162
+ if (obj instanceof Date) {
163
+ return {
164
+ __type: 'Date',
165
+ value: obj.toISOString()
166
+ };
167
+ }
168
+
169
+ if (obj instanceof Error) {
170
+ return {
171
+ __type: 'Error',
172
+ name: obj.name,
173
+ message: obj.message,
174
+ stack: obj.stack,
175
+ cause: obj.cause ? this.makeSerializable(obj.cause, `${path}.cause`) : undefined
176
+ };
177
+ }
178
+
179
+ if (obj instanceof RegExp) {
180
+ return {
181
+ __type: 'RegExp',
182
+ source: obj.source,
183
+ flags: obj.flags
184
+ };
185
+ }
186
+
187
+ // Arrays
188
+ if (Array.isArray(obj)) {
189
+ // Verificar referencia circular
190
+ if (this.seen.has(obj)) {
191
+ const refId = this.circularRefs.get(obj);
192
+ return {
193
+ __circular: true,
194
+ refId: refId
195
+ };
196
+ }
197
+
198
+ this.seen.add(obj);
199
+ const refId = `ref_${++this.refCounter}`;
200
+ this.circularRefs.set(obj, refId);
201
+
202
+ return obj.map((item, index) =>
203
+ this.makeSerializable(item, `${path}[${index}]`)
204
+ );
205
+ }
206
+
207
+ // Objetos
208
+ if (typeof obj === 'object') {
209
+ // Verificar referencia circular
210
+ if (this.seen.has(obj)) {
211
+ const refId = this.circularRefs.get(obj);
212
+ return {
213
+ __circular: true,
214
+ refId: refId
215
+ };
216
+ }
217
+
218
+ this.seen.add(obj);
219
+ const refId = `ref_${++this.refCounter}`;
220
+ this.circularRefs.set(obj, refId);
221
+
222
+ const result = {};
223
+
224
+ // Procesar propiedades del objeto
225
+ for (const key in obj) {
226
+ if (obj.hasOwnProperty(key)) {
227
+ try {
228
+ const value = obj[key];
229
+ const safeValue = this.makeSerializable(value, `${path}.${key}`);
230
+ result[key] = safeValue;
231
+ } catch (error) {
232
+ // Si falla la serialización de una propiedad, la omitimos
233
+ result[key] = {
234
+ __serializationError: true,
235
+ error: error.message,
236
+ propertyName: key
237
+ };
238
+ }
239
+ }
240
+ }
241
+
242
+ // Procesar símbolos si están disponibles
243
+ if (Object.getOwnPropertySymbols) {
244
+ const symbols = Object.getOwnPropertySymbols(obj);
245
+ for (const symbol of symbols) {
246
+ try {
247
+ const value = obj[symbol];
248
+ const safeValue = this.makeSerializable(value, `${path}[Symbol(${symbol.description})]`);
249
+ result[`__symbol_${symbol.description || 'anonymous'}`] = safeValue;
250
+ } catch (error) {
251
+ result[`__symbol_${symbol.description || 'anonymous'}`] = {
252
+ __serializationError: true,
253
+ error: error.message,
254
+ symbolName: symbol.description || 'anonymous'
255
+ };
256
+ }
257
+ }
258
+ }
259
+
260
+ return result;
261
+ }
262
+
263
+ // Funciones y otros tipos
264
+ if (typeof obj === 'function') {
265
+ return {
266
+ __type: 'Function',
267
+ name: obj.name || 'anonymous',
268
+ length: obj.length,
269
+ toString: obj.toString().substring(0, 200) + '...'
270
+ };
271
+ }
272
+
273
+ // Fallback para otros tipos
274
+ return {
275
+ __type: 'Unknown',
276
+ constructor: obj.constructor ? obj.constructor.name : 'Unknown',
277
+ toString: String(obj).substring(0, 200) + '...'
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Deserializa un objeto serializado con referencias circulares
283
+ * @param {string} jsonString - JSON string a deserializar
284
+ * @returns {any} Objeto deserializado
285
+ */
286
+ deserialize(jsonString) {
287
+ try {
288
+ const parsed = JSON.parse(jsonString);
289
+ return this.restoreCircularRefs(parsed);
290
+ } catch (error) {
291
+ console.error('SyntropyFront: Error en deserialización:', error);
292
+ return null;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Restaura referencias circulares en un objeto deserializado
298
+ * @param {any} obj - Objeto a restaurar
299
+ * @param {Map} refs - Mapa de referencias
300
+ * @returns {any} Objeto con referencias restauradas
301
+ */
302
+ restoreCircularRefs(obj, refs = new Map()) {
303
+ if (obj === null || obj === undefined) {
304
+ return obj;
305
+ }
306
+
307
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
308
+ return obj;
309
+ }
310
+
311
+ // Restaurar tipos especiales
312
+ if (obj.__type === 'Date') {
313
+ return new Date(obj.value);
314
+ }
315
+
316
+ if (obj.__type === 'Error') {
317
+ const error = new Error(obj.message);
318
+ error.name = obj.name;
319
+ error.stack = obj.stack;
320
+ if (obj.cause) {
321
+ error.cause = this.restoreCircularRefs(obj.cause, refs);
322
+ }
323
+ return error;
324
+ }
325
+
326
+ if (obj.__type === 'RegExp') {
327
+ return new RegExp(obj.source, obj.flags);
328
+ }
329
+
330
+ if (obj.__type === 'Function') {
331
+ // No podemos restaurar funciones completamente, devolvemos info
332
+ return `[Function: ${obj.name}]`;
333
+ }
334
+
335
+ // Arrays
336
+ if (Array.isArray(obj)) {
337
+ const result = [];
338
+ refs.set(obj, result);
339
+
340
+ for (let i = 0; i < obj.length; i++) {
341
+ if (obj[i] && obj[i].__circular) {
342
+ const refId = obj[i].refId;
343
+ if (refs.has(refId)) {
344
+ result[i] = refs.get(refId);
345
+ } else {
346
+ result[i] = null; // Referencia no encontrada
347
+ }
348
+ } else {
349
+ result[i] = this.restoreCircularRefs(obj[i], refs);
350
+ }
351
+ }
352
+
353
+ return result;
354
+ }
355
+
356
+ // Objetos
357
+ if (typeof obj === 'object') {
358
+ const result = {};
359
+ refs.set(obj, result);
360
+
361
+ for (const key in obj) {
362
+ if (obj.hasOwnProperty(key)) {
363
+ if (key.startsWith('__')) {
364
+ // Propiedades especiales
365
+ continue;
366
+ }
367
+
368
+ const value = obj[key];
369
+ if (value && value.__circular) {
370
+ const refId = value.refId;
371
+ if (refs.has(refId)) {
372
+ result[key] = refs.get(refId);
373
+ } else {
374
+ result[key] = null; // Referencia no encontrada
375
+ }
376
+ } else {
377
+ result[key] = this.restoreCircularRefs(value, refs);
378
+ }
379
+ }
380
+ }
381
+
382
+ return result;
383
+ }
384
+
385
+ return obj;
386
+ }
387
+
388
+ /**
389
+ * Serializa de forma segura para logging (versión simplificada)
390
+ * @param {any} obj - Objeto a serializar
391
+ * @returns {string} JSON string seguro para logs
392
+ */
393
+ serializeForLogging(obj) {
394
+ try {
395
+ return this.serialize(obj);
396
+ } catch (error) {
397
+ return JSON.stringify({
398
+ __logError: true,
399
+ message: 'Error serializando para logging',
400
+ originalError: error.message,
401
+ timestamp: new Date().toISOString()
402
+ });
403
+ }
404
+ }
405
+ }
406
+
407
+ // Instancia singleton
408
+ const robustSerializer = new RobustSerializer();
409
+
410
+ /**
411
+ * Agent - Envía datos de trazabilidad al backend
412
+ * Implementa reintentos con backoff exponencial y buffer persistente
413
+ */
414
+ class Agent {
415
+ constructor() {
416
+ this.endpoint = null;
417
+ this.headers = {
418
+ 'Content-Type': 'application/json'
419
+ };
420
+ this.batchSize = 10;
421
+ this.batchTimeout = null; // Por defecto = solo errores
422
+ this.queue = [];
423
+ this.batchTimer = null;
424
+ this.isEnabled = false;
425
+ this.sendBreadcrumbs = false; // Por defecto = solo errores
426
+ this.encrypt = null; // Callback de encriptación opcional
427
+
428
+ // Sistema de reintentos
429
+ this.retryQueue = []; // Cola de reintentos
430
+ this.retryTimer = null;
431
+ this.maxRetries = 5;
432
+ this.baseDelay = 1000; // 1 segundo
433
+ this.maxDelay = 30000; // 30 segundos
434
+
435
+ // Buffer persistente
436
+ this.usePersistentBuffer = false;
437
+ this.dbName = 'SyntropyFrontBuffer';
438
+ this.dbVersion = 1;
439
+ this.storeName = 'failedItems';
440
+
441
+ // Inicializar buffer persistente si está disponible
442
+ this.initPersistentBuffer();
443
+ }
444
+
445
+ /**
446
+ * Inicializa el buffer persistente (IndexedDB)
447
+ */
448
+ async initPersistentBuffer() {
449
+ try {
450
+ if (!window.indexedDB) {
451
+ console.warn('SyntropyFront: IndexedDB no disponible, usando solo memoria');
452
+ return;
453
+ }
454
+
455
+ const request = indexedDB.open(this.dbName, this.dbVersion);
456
+
457
+ request.onerror = () => {
458
+ console.warn('SyntropyFront: Error abriendo IndexedDB, usando solo memoria');
459
+ };
460
+
461
+ request.onupgradeneeded = (event) => {
462
+ const db = event.target.result;
463
+ if (!db.objectStoreNames.contains(this.storeName)) {
464
+ db.createObjectStore(this.storeName, { keyPath: 'id', autoIncrement: true });
465
+ }
466
+ };
467
+
468
+ request.onsuccess = () => {
469
+ this.db = request.result;
470
+ this.usePersistentBuffer = true;
471
+ console.log('SyntropyFront: Buffer persistente inicializado');
472
+
473
+ // Intentar enviar items fallidos al inicializar
474
+ this.retryFailedItems();
475
+ };
476
+ } catch (error) {
477
+ console.warn('SyntropyFront: Error inicializando buffer persistente:', error);
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Guarda items fallidos en el buffer persistente
483
+ */
484
+ async saveToPersistentBuffer(items) {
485
+ if (!this.usePersistentBuffer || !this.db) return;
486
+
487
+ try {
488
+ const transaction = this.db.transaction([this.storeName], 'readwrite');
489
+ const store = transaction.objectStore(this.storeName);
490
+
491
+ // ✅ SERIALIZACIÓN ROBUSTA: Serializar items antes de guardar
492
+ let serializedItems;
493
+ try {
494
+ serializedItems = robustSerializer.serialize(items);
495
+ } catch (error) {
496
+ console.error('SyntropyFront: Error serializando items para buffer:', error);
497
+ serializedItems = JSON.stringify({
498
+ __serializationError: true,
499
+ error: error.message,
500
+ timestamp: new Date().toISOString(),
501
+ fallbackData: 'Items no serializables'
502
+ });
503
+ }
504
+
505
+ const item = {
506
+ items: serializedItems, // Guardar como string serializado
507
+ timestamp: new Date().toISOString(),
508
+ retryCount: 0
509
+ };
510
+
511
+ await store.add(item);
512
+ console.log('SyntropyFront: Items guardados en buffer persistente');
513
+ } catch (error) {
514
+ console.error('SyntropyFront: Error guardando en buffer persistente:', error);
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Obtiene items fallidos del buffer persistente
520
+ */
521
+ async getFromPersistentBuffer() {
522
+ if (!this.usePersistentBuffer || !this.db) return [];
523
+
524
+ try {
525
+ const transaction = this.db.transaction([this.storeName], 'readonly');
526
+ const store = transaction.objectStore(this.storeName);
527
+ const request = store.getAll();
528
+
529
+ return new Promise((resolve, reject) => {
530
+ request.onsuccess = () => resolve(request.result);
531
+ request.onerror = () => reject(request.error);
532
+ });
533
+ } catch (error) {
534
+ console.error('SyntropyFront: Error obteniendo del buffer persistente:', error);
535
+ return [];
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Remueve items del buffer persistente
541
+ */
542
+ async removeFromPersistentBuffer(id) {
543
+ if (!this.usePersistentBuffer || !this.db) return;
544
+
545
+ try {
546
+ const transaction = this.db.transaction([this.storeName], 'readwrite');
547
+ const store = transaction.objectStore(this.storeName);
548
+ await store.delete(id);
549
+ } catch (error) {
550
+ console.error('SyntropyFront: Error removiendo del buffer persistente:', error);
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Intenta enviar items fallidos del buffer persistente
556
+ */
557
+ async retryFailedItems() {
558
+ if (!this.usePersistentBuffer) return;
559
+
560
+ const failedItems = await this.getFromPersistentBuffer();
561
+
562
+ for (const item of failedItems) {
563
+ if (item.retryCount < this.maxRetries) {
564
+ // ✅ DESERIALIZACIÓN ROBUSTA: Deserializar items del buffer
565
+ let deserializedItems;
566
+ try {
567
+ if (typeof item.items === 'string') {
568
+ deserializedItems = robustSerializer.deserialize(item.items);
569
+ } else {
570
+ deserializedItems = item.items; // Ya deserializado
571
+ }
572
+ } catch (error) {
573
+ console.error('SyntropyFront: Error deserializando items del buffer:', error);
574
+ // Saltar este item y removerlo del buffer
575
+ await this.removeFromPersistentBuffer(item.id);
576
+ continue;
577
+ }
578
+
579
+ this.addToRetryQueue(deserializedItems, item.retryCount + 1, item.id);
580
+ } else {
581
+ console.warn('SyntropyFront: Item excedió máximo de reintentos, removiendo del buffer');
582
+ await this.removeFromPersistentBuffer(item.id);
583
+ }
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Configura el agent
589
+ * @param {Object} config - Configuración del agent
590
+ * @param {string} config.endpoint - URL del endpoint para enviar datos
591
+ * @param {Object} [config.headers] - Headers adicionales
592
+ * @param {number} [config.batchSize] - Tamaño del batch
593
+ * @param {number} [config.batchTimeout] - Timeout del batch en ms (si existe = modo completo)
594
+ * @param {Function} [config.encrypt] - Callback para encriptar datos antes de enviar
595
+ * @param {boolean} [config.usePersistentBuffer] - Usar buffer persistente (default: true)
596
+ * @param {number} [config.maxRetries] - Máximo número de reintentos (default: 5)
597
+ */
598
+ configure(config) {
599
+ this.endpoint = config.endpoint;
600
+ this.headers = { ...this.headers, ...config.headers };
601
+ this.batchSize = config.batchSize || this.batchSize;
602
+ this.batchTimeout = config.batchTimeout; // Si existe = modo completo
603
+ this.isEnabled = !!config.endpoint;
604
+ this.encrypt = config.encrypt || null; // Callback de encriptación
605
+ this.usePersistentBuffer = config.usePersistentBuffer !== false; // Por defecto: true
606
+ this.maxRetries = config.maxRetries || this.maxRetries;
607
+
608
+ // Lógica simple: si hay batchTimeout = enviar breadcrumbs, sino = solo errores
609
+ this.sendBreadcrumbs = !!config.batchTimeout;
610
+ }
611
+
612
+ /**
613
+ * Envía un error al backend
614
+ * @param {Object} errorPayload - Payload del error
615
+ * @param {Object} context - Contexto adicional (opcional)
616
+ */
617
+ sendError(errorPayload, context = null) {
618
+ if (!this.isEnabled) {
619
+ console.warn('SyntropyFront Agent: No configurado, error no enviado');
620
+ return;
621
+ }
622
+
623
+ // Agregar contexto si está disponible
624
+ const payloadWithContext = context ? {
625
+ ...errorPayload,
626
+ context: context
627
+ } : errorPayload;
628
+
629
+ // Aplicar encriptación si está configurada
630
+ const dataToSend = this.encrypt ? this.encrypt(payloadWithContext) : payloadWithContext;
631
+
632
+ this.addToQueue({
633
+ type: 'error',
634
+ data: dataToSend,
635
+ timestamp: new Date().toISOString()
636
+ });
637
+ }
638
+
639
+ /**
640
+ * Envía breadcrumbs al backend
641
+ * @param {Array} breadcrumbs - Lista de breadcrumbs
642
+ */
643
+ sendBreadcrumbs(breadcrumbs) {
644
+ // Solo enviar breadcrumbs si está habilitado (batchTimeout configurado)
645
+ if (!this.isEnabled || !this.sendBreadcrumbs || !breadcrumbs.length) {
646
+ return;
647
+ }
648
+
649
+ // Aplicar encriptación si está configurada
650
+ const dataToSend = this.encrypt ? this.encrypt(breadcrumbs) : breadcrumbs;
651
+
652
+ this.addToQueue({
653
+ type: 'breadcrumbs',
654
+ data: dataToSend,
655
+ timestamp: new Date().toISOString()
656
+ });
657
+ }
658
+
659
+ /**
660
+ * Añade un item a la cola de envío
661
+ * @param {Object} item - Item a añadir
662
+ */
663
+ addToQueue(item) {
664
+ this.queue.push(item);
665
+
666
+ // Enviar inmediatamente si alcanza el tamaño del batch
667
+ if (this.queue.length >= this.batchSize) {
668
+ this.flush();
669
+ } else if (this.batchTimeout && !this.batchTimer) {
670
+ // Solo programar timeout si batchTimeout está configurado
671
+ this.batchTimer = setTimeout(() => {
672
+ this.flush();
673
+ }, this.batchTimeout);
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Añade items a la cola de reintentos
679
+ * @param {Array} items - Items a reintentar
680
+ * @param {number} retryCount - Número de reintento
681
+ * @param {number} persistentId - ID en buffer persistente (opcional)
682
+ */
683
+ addToRetryQueue(items, retryCount = 1, persistentId = null) {
684
+ const delay = Math.min(this.baseDelay * Math.pow(2, retryCount - 1), this.maxDelay);
685
+
686
+ this.retryQueue.push({
687
+ items,
688
+ retryCount,
689
+ persistentId,
690
+ nextRetry: Date.now() + delay
691
+ });
692
+
693
+ this.scheduleRetry();
694
+ }
695
+
696
+ /**
697
+ * Programa el próximo reintento
698
+ */
699
+ scheduleRetry() {
700
+ if (this.retryTimer) return;
701
+
702
+ const nextItem = this.retryQueue.find(item => item.nextRetry <= Date.now());
703
+ if (!nextItem) return;
704
+
705
+ this.retryTimer = setTimeout(() => {
706
+ this.processRetryQueue();
707
+ }, Math.max(0, nextItem.nextRetry - Date.now()));
708
+ }
709
+
710
+ /**
711
+ * Procesa la cola de reintentos
712
+ */
713
+ async processRetryQueue() {
714
+ this.retryTimer = null;
715
+
716
+ const now = Date.now();
717
+ const itemsToRetry = this.retryQueue.filter(item => item.nextRetry <= now);
718
+
719
+ for (const item of itemsToRetry) {
720
+ try {
721
+ await this.sendToBackend(item.items);
722
+
723
+ // ✅ Éxito: remover de cola de reintentos
724
+ this.retryQueue = this.retryQueue.filter(q => q !== item);
725
+
726
+ // Remover del buffer persistente si existe
727
+ if (item.persistentId) {
728
+ await this.removeFromPersistentBuffer(item.persistentId);
729
+ }
730
+
731
+ console.log(`SyntropyFront: Reintento exitoso después de ${item.retryCount} intentos`);
732
+ } catch (error) {
733
+ console.warn(`SyntropyFront: Reintento ${item.retryCount} falló:`, error);
734
+
735
+ if (item.retryCount >= this.maxRetries) {
736
+ // ❌ Máximo de reintentos alcanzado
737
+ this.retryQueue = this.retryQueue.filter(q => q !== item);
738
+ console.error('SyntropyFront: Item excedió máximo de reintentos, datos perdidos');
739
+ } else {
740
+ // Programar próximo reintento
741
+ item.retryCount++;
742
+ item.nextRetry = Date.now() + Math.min(
743
+ this.baseDelay * Math.pow(2, item.retryCount - 1),
744
+ this.maxDelay
745
+ );
746
+ }
747
+ }
748
+ }
749
+
750
+ // Programar próximo reintento si quedan items
751
+ if (this.retryQueue.length > 0) {
752
+ this.scheduleRetry();
753
+ }
754
+ }
755
+
756
+ /**
757
+ * Envía todos los items en cola
758
+ */
759
+ async flush() {
760
+ if (this.queue.length === 0) return;
761
+
762
+ const itemsToSend = [...this.queue];
763
+ this.queue = [];
764
+
765
+ if (this.batchTimer) {
766
+ clearTimeout(this.batchTimer);
767
+ this.batchTimer = null;
768
+ }
769
+
770
+ try {
771
+ await this.sendToBackend(itemsToSend);
772
+ console.log('SyntropyFront: Datos enviados exitosamente');
773
+ } catch (error) {
774
+ console.error('SyntropyFront Agent: Error enviando datos:', error);
775
+
776
+ // ✅ REINTENTOS: Agregar a cola de reintentos
777
+ this.addToRetryQueue(itemsToSend);
778
+
779
+ // ✅ BUFFER PERSISTENTE: Guardar en IndexedDB
780
+ if (this.usePersistentBuffer) {
781
+ await this.saveToPersistentBuffer(itemsToSend);
782
+ }
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Envía datos al backend
788
+ * @param {Array} items - Items a enviar
789
+ */
790
+ async sendToBackend(items) {
791
+ const payload = {
792
+ timestamp: new Date().toISOString(),
793
+ items: items
794
+ };
795
+
796
+ // ✅ SERIALIZACIÓN ROBUSTA: Usar serializador que maneja referencias circulares
797
+ let serializedPayload;
798
+ try {
799
+ serializedPayload = robustSerializer.serialize(payload);
800
+ } catch (error) {
801
+ console.error('SyntropyFront: Error en serialización del payload:', error);
802
+
803
+ // Fallback: intentar serialización básica con información de error
804
+ serializedPayload = JSON.stringify({
805
+ __serializationError: true,
806
+ error: error.message,
807
+ timestamp: new Date().toISOString(),
808
+ itemsCount: items.length,
809
+ fallbackData: 'Serialización falló, datos no enviados'
810
+ });
811
+ }
812
+
813
+ const response = await fetch(this.endpoint, {
814
+ method: 'POST',
815
+ headers: this.headers,
816
+ body: serializedPayload
817
+ });
818
+
819
+ if (!response.ok) {
820
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
821
+ }
822
+
823
+ return response.json();
824
+ }
825
+
826
+ /**
827
+ * Fuerza el envío inmediato de todos los datos pendientes
828
+ */
829
+ async forceFlush() {
830
+ await this.flush();
831
+
832
+ // También intentar enviar items en cola de reintentos
833
+ if (this.retryQueue.length > 0) {
834
+ console.log('SyntropyFront: Intentando enviar items en cola de reintentos...');
835
+ await this.processRetryQueue();
836
+ }
837
+ }
838
+
839
+ /**
840
+ * Obtiene estadísticas del agent
841
+ * @returns {Object} Estadísticas
842
+ */
843
+ getStats() {
844
+ return {
845
+ queueLength: this.queue.length,
846
+ retryQueueLength: this.retryQueue.length,
847
+ isEnabled: this.isEnabled,
848
+ usePersistentBuffer: this.usePersistentBuffer,
849
+ maxRetries: this.maxRetries
850
+ };
851
+ }
852
+
853
+ /**
854
+ * Desactiva el agent
855
+ */
856
+ disable() {
857
+ this.isEnabled = false;
858
+ this.queue = [];
859
+ this.retryQueue = [];
860
+
861
+ if (this.batchTimer) {
862
+ clearTimeout(this.batchTimer);
863
+ this.batchTimer = null;
864
+ }
865
+
866
+ if (this.retryTimer) {
867
+ clearTimeout(this.retryTimer);
868
+ this.retryTimer = null;
869
+ }
870
+ }
871
+ }
872
+
873
+ // Instancia singleton
874
+ const agent = new Agent();
875
+
876
+ /**
877
+ * ContextCollector - Recolector dinámico de contexto
878
+ * Sistema elegante para recolectar datos según lo que pida el usuario
879
+ * Por defecto: Sets curados y seguros
880
+ * Configuración específica: El usuario elige exactamente qué quiere
881
+ */
882
+ class ContextCollector {
883
+ constructor() {
884
+ // Sets curados por defecto (seguros y útiles)
885
+ this.defaultContexts = {
886
+ device: {
887
+ userAgent: () => navigator.userAgent,
888
+ language: () => navigator.language,
889
+ screen: () => ({
890
+ width: window.screen.width,
891
+ height: window.screen.height
892
+ }),
893
+ timezone: () => Intl.DateTimeFormat().resolvedOptions().timeZone
894
+ },
895
+ window: {
896
+ url: () => window.location.href,
897
+ viewport: () => ({
898
+ width: window.innerWidth,
899
+ height: window.innerHeight
900
+ }),
901
+ title: () => document.title
902
+ },
903
+ session: {
904
+ sessionId: () => this.generateSessionId(),
905
+ pageLoadTime: () => performance.now()
906
+ },
907
+ ui: {
908
+ visibility: () => document.visibilityState,
909
+ activeElement: () => document.activeElement ? {
910
+ tagName: document.activeElement.tagName
911
+ } : null
912
+ },
913
+ network: {
914
+ online: () => navigator.onLine,
915
+ connection: () => navigator.connection ? {
916
+ effectiveType: navigator.connection.effectiveType
917
+ } : null
918
+ }
919
+ };
920
+
921
+ // Mapeo completo de todos los campos disponibles
922
+ this.allFields = {
923
+ device: {
924
+ userAgent: () => navigator.userAgent,
925
+ language: () => navigator.language,
926
+ languages: () => navigator.languages,
927
+ screen: () => ({
928
+ width: window.screen.width,
929
+ height: window.screen.height,
930
+ availWidth: window.screen.availWidth,
931
+ availHeight: window.screen.availHeight,
932
+ colorDepth: window.screen.colorDepth,
933
+ pixelDepth: window.screen.pixelDepth
934
+ }),
935
+ timezone: () => Intl.DateTimeFormat().resolvedOptions().timeZone,
936
+ cookieEnabled: () => navigator.cookieEnabled,
937
+ doNotTrack: () => navigator.doNotTrack
938
+ },
939
+ window: {
940
+ url: () => window.location.href,
941
+ pathname: () => window.location.pathname,
942
+ search: () => window.location.search,
943
+ hash: () => window.location.hash,
944
+ referrer: () => document.referrer,
945
+ title: () => document.title,
946
+ viewport: () => ({
947
+ width: window.innerWidth,
948
+ height: window.innerHeight
949
+ })
950
+ },
951
+ storage: {
952
+ localStorage: () => {
953
+ const keys = Object.keys(localStorage);
954
+ return {
955
+ keys: keys.length,
956
+ size: JSON.stringify(localStorage).length,
957
+ keyNames: keys // Solo nombres, no valores
958
+ };
959
+ },
960
+ sessionStorage: () => {
961
+ const keys = Object.keys(sessionStorage);
962
+ return {
963
+ keys: keys.length,
964
+ size: JSON.stringify(sessionStorage).length,
965
+ keyNames: keys // Solo nombres, no valores
966
+ };
967
+ }
968
+ },
969
+ network: {
970
+ online: () => navigator.onLine,
971
+ connection: () => navigator.connection ? {
972
+ effectiveType: navigator.connection.effectiveType,
973
+ downlink: navigator.connection.downlink,
974
+ rtt: navigator.connection.rtt
975
+ } : null
976
+ },
977
+ ui: {
978
+ focused: () => document.hasFocus(),
979
+ visibility: () => document.visibilityState,
980
+ activeElement: () => document.activeElement ? {
981
+ tagName: document.activeElement.tagName,
982
+ id: document.activeElement.id,
983
+ className: document.activeElement.className
984
+ } : null
985
+ },
986
+ performance: {
987
+ memory: () => window.performance && window.performance.memory ? {
988
+ used: Math.round(window.performance.memory.usedJSHeapSize / 1048576),
989
+ total: Math.round(window.performance.memory.totalJSHeapSize / 1048576),
990
+ limit: Math.round(window.performance.memory.jsHeapSizeLimit / 1048576)
991
+ } : null,
992
+ timing: () => window.performance ? {
993
+ navigationStart: window.performance.timing.navigationStart,
994
+ loadEventEnd: window.performance.timing.loadEventEnd
995
+ } : null
996
+ },
997
+ session: {
998
+ sessionId: () => this.generateSessionId(),
999
+ startTime: () => new Date().toISOString(),
1000
+ pageLoadTime: () => performance.now()
1001
+ }
1002
+ };
1003
+ }
1004
+
1005
+ /**
1006
+ * Recolecta contexto según la configuración
1007
+ * @param {Object} contextConfig - Configuración de contexto
1008
+ * @returns {Object} Contexto recolectado
1009
+ */
1010
+ collect(contextConfig = {}) {
1011
+ const context = {};
1012
+
1013
+ Object.entries(contextConfig).forEach(([contextType, config]) => {
1014
+ try {
1015
+ if (config === true) {
1016
+ // Usar set curado por defecto
1017
+ context[contextType] = this.collectDefaultContext(contextType);
1018
+ } else if (Array.isArray(config)) {
1019
+ // Configuración específica: array de campos
1020
+ context[contextType] = this.collectSpecificFields(contextType, config);
1021
+ } else if (config === false) {
1022
+ // Explícitamente deshabilitado
1023
+ // No hacer nada
1024
+ } else {
1025
+ console.warn(`SyntropyFront: Configuración de contexto inválida para ${contextType}:`, config);
1026
+ }
1027
+ } catch (error) {
1028
+ console.warn(`SyntropyFront: Error recolectando contexto ${contextType}:`, error);
1029
+ context[contextType] = { error: 'Failed to collect' };
1030
+ }
1031
+ });
1032
+
1033
+ return context;
1034
+ }
1035
+
1036
+ /**
1037
+ * Recolecta el set curado por defecto
1038
+ * @param {string} contextType - Tipo de contexto
1039
+ * @returns {Object} Contexto por defecto
1040
+ */
1041
+ collectDefaultContext(contextType) {
1042
+ const defaultContext = this.defaultContexts[contextType];
1043
+ if (!defaultContext) {
1044
+ console.warn(`SyntropyFront: No hay set por defecto para ${contextType}`);
1045
+ return {};
1046
+ }
1047
+
1048
+ const result = {};
1049
+ Object.entries(defaultContext).forEach(([field, getter]) => {
1050
+ try {
1051
+ result[field] = getter();
1052
+ } catch (error) {
1053
+ console.warn(`SyntropyFront: Error recolectando campo ${field} de ${contextType}:`, error);
1054
+ result[field] = null;
1055
+ }
1056
+ });
1057
+
1058
+ return result;
1059
+ }
1060
+
1061
+ /**
1062
+ * Recolecta campos específicos
1063
+ * @param {string} contextType - Tipo de contexto
1064
+ * @param {Array} fields - Campos específicos a recolectar
1065
+ * @returns {Object} Contexto específico
1066
+ */
1067
+ collectSpecificFields(contextType, fields) {
1068
+ const allFields = this.allFields[contextType];
1069
+ if (!allFields) {
1070
+ console.warn(`SyntropyFront: Tipo de contexto desconocido: ${contextType}`);
1071
+ return {};
1072
+ }
1073
+
1074
+ const result = {};
1075
+ fields.forEach(field => {
1076
+ try {
1077
+ if (allFields[field]) {
1078
+ result[field] = allFields[field]();
1079
+ } else {
1080
+ console.warn(`SyntropyFront: Campo ${field} no disponible en ${contextType}`);
1081
+ }
1082
+ } catch (error) {
1083
+ console.warn(`SyntropyFront: Error recolectando campo ${field} de ${contextType}:`, error);
1084
+ result[field] = null;
1085
+ }
1086
+ });
1087
+
1088
+ return result;
1089
+ }
1090
+
1091
+ /**
1092
+ * Genera un ID de sesión simple
1093
+ */
1094
+ generateSessionId() {
1095
+ if (!this._sessionId) {
1096
+ this._sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1097
+ }
1098
+ return this._sessionId;
1099
+ }
1100
+
1101
+ /**
1102
+ * Obtiene la lista de tipos de contexto disponibles
1103
+ * @returns {Array} Tipos disponibles
1104
+ */
1105
+ getAvailableTypes() {
1106
+ return Object.keys(this.allFields);
1107
+ }
1108
+
1109
+ /**
1110
+ * Obtiene la lista de campos disponibles para un tipo de contexto
1111
+ * @param {string} contextType - Tipo de contexto
1112
+ * @returns {Array} Campos disponibles
1113
+ */
1114
+ getAvailableFields(contextType) {
1115
+ const fields = this.allFields[contextType];
1116
+ return fields ? Object.keys(fields) : [];
1117
+ }
1118
+
1119
+ /**
1120
+ * Obtiene información sobre los sets por defecto
1121
+ * @returns {Object} Información de sets por defecto
1122
+ */
1123
+ getDefaultContextsInfo() {
1124
+ const info = {};
1125
+ Object.entries(this.defaultContexts).forEach(([type, fields]) => {
1126
+ info[type] = Object.keys(fields);
1127
+ });
1128
+ return info;
1129
+ }
1130
+ }
1131
+
1132
+ // Instancia singleton
1133
+ const contextCollector = new ContextCollector();
1134
+
1135
+ /**
1136
+ * Interceptors - Observadores que capturan eventos automáticamente
1137
+ * Implementa Chaining Pattern para coexistir con otros APMs
1138
+ */
1139
+ class Interceptors {
1140
+ constructor() {
1141
+ this.isInitialized = false;
1142
+ this.config = {
1143
+ captureClicks: true,
1144
+ captureFetch: true,
1145
+ captureErrors: true,
1146
+ captureUnhandledRejections: true
1147
+ };
1148
+ this.contextTypes = [];
1149
+
1150
+ // Referencias originales para restaurar en destroy()
1151
+ this.originalHandlers = {
1152
+ fetch: null,
1153
+ onerror: null,
1154
+ onunhandledrejection: null
1155
+ };
1156
+
1157
+ // Event listeners para limpiar
1158
+ this.eventListeners = new Map();
1159
+ }
1160
+
1161
+ /**
1162
+ * Configura los interceptores
1163
+ * @param {Object} config - Configuración de interceptores
1164
+ */
1165
+ configure(config) {
1166
+ this.config = { ...this.config, ...config };
1167
+ this.contextTypes = config.context || [];
1168
+ }
1169
+
1170
+ /**
1171
+ * Inicializa todos los interceptores
1172
+ */
1173
+ init() {
1174
+ if (this.isInitialized) {
1175
+ console.warn('SyntropyFront: Interceptors ya están inicializados');
1176
+ return;
1177
+ }
1178
+
1179
+ if (this.config.captureClicks) {
1180
+ this.setupClickInterceptor();
1181
+ }
1182
+
1183
+ if (this.config.captureFetch) {
1184
+ this.setupFetchInterceptor();
1185
+ }
1186
+
1187
+ if (this.config.captureErrors || this.config.captureUnhandledRejections) {
1188
+ this.setupErrorInterceptors();
1189
+ }
1190
+
1191
+ this.isInitialized = true;
1192
+ console.log('SyntropyFront: Interceptors inicializados con Chaining Pattern');
1193
+ }
1194
+
1195
+ /**
1196
+ * Intercepta clics de usuario
1197
+ */
1198
+ setupClickInterceptor() {
1199
+ const clickHandler = (event) => {
1200
+ const el = event.target;
1201
+ if (!el) return;
1202
+
1203
+ // Genera un selector CSS simple para identificar el elemento
1204
+ let selector = el.tagName.toLowerCase();
1205
+ if (el.id) {
1206
+ selector += `#${el.id}`;
1207
+ } else if (el.className && typeof el.className === 'string') {
1208
+ selector += `.${el.className.split(' ').filter(Boolean).join('.')}`;
1209
+ }
1210
+
1211
+ breadcrumbStore.add({
1212
+ category: 'ui',
1213
+ message: `Usuario hizo click en '${selector}'`,
1214
+ data: {
1215
+ selector,
1216
+ tagName: el.tagName,
1217
+ id: el.id,
1218
+ className: el.className
1219
+ }
1220
+ });
1221
+ };
1222
+
1223
+ // Guardar referencia para limpiar después
1224
+ this.eventListeners.set('click', clickHandler);
1225
+ document.addEventListener('click', clickHandler, true);
1226
+ }
1227
+
1228
+ /**
1229
+ * Intercepta llamadas de red (fetch) con Chaining
1230
+ */
1231
+ setupFetchInterceptor() {
1232
+ // Guardar referencia original
1233
+ this.originalHandlers.fetch = window.fetch;
1234
+
1235
+ // Crear nuevo handler que encadena con el original
1236
+ const syntropyFetchHandler = (...args) => {
1237
+ const url = args[0] instanceof Request ? args[0].url : args[0];
1238
+ const method = args[0] instanceof Request ? args[0].method : (args[1]?.method || 'GET');
1239
+
1240
+ breadcrumbStore.add({
1241
+ category: 'network',
1242
+ message: `Request: ${method} ${url}`,
1243
+ data: {
1244
+ url,
1245
+ method,
1246
+ timestamp: Date.now()
1247
+ }
1248
+ });
1249
+
1250
+ // ✅ CHAINING: Llamar al handler original
1251
+ return this.originalHandlers.fetch.apply(window, args);
1252
+ };
1253
+
1254
+ // Sobrescribir con el nuevo handler
1255
+ window.fetch = syntropyFetchHandler;
1256
+ }
1257
+
1258
+ /**
1259
+ * Intercepta errores globales con Chaining
1260
+ */
1261
+ setupErrorInterceptors() {
1262
+ if (this.config.captureErrors) {
1263
+ // Guardar referencia original
1264
+ this.originalHandlers.onerror = window.onerror;
1265
+
1266
+ // Crear nuevo handler que encadena con el original
1267
+ const syntropyErrorHandler = (message, source, lineno, colno, error) => {
1268
+ const errorPayload = {
1269
+ type: 'uncaught_exception',
1270
+ error: {
1271
+ message,
1272
+ source,
1273
+ lineno,
1274
+ colno,
1275
+ stack: error?.stack
1276
+ },
1277
+ breadcrumbs: breadcrumbStore.getAll(),
1278
+ timestamp: new Date().toISOString()
1279
+ };
1280
+
1281
+ this.handleError(errorPayload);
1282
+
1283
+ // ✅ CHAINING: Llamar al handler original si existe
1284
+ if (this.originalHandlers.onerror) {
1285
+ try {
1286
+ return this.originalHandlers.onerror(message, source, lineno, colno, error);
1287
+ } catch (originalError) {
1288
+ console.warn('SyntropyFront: Error en handler original:', originalError);
1289
+ return false;
1290
+ }
1291
+ }
1292
+
1293
+ return false; // No prevenir el error por defecto
1294
+ };
1295
+
1296
+ // Sobrescribir con el nuevo handler
1297
+ window.onerror = syntropyErrorHandler;
1298
+ }
1299
+
1300
+ if (this.config.captureUnhandledRejections) {
1301
+ // Guardar referencia original
1302
+ this.originalHandlers.onunhandledrejection = window.onunhandledrejection;
1303
+
1304
+ // Crear nuevo handler que encadena con el original
1305
+ const syntropyRejectionHandler = (event) => {
1306
+ const errorPayload = {
1307
+ type: 'unhandled_rejection',
1308
+ error: {
1309
+ message: event.reason?.message || 'Rechazo de promesa sin mensaje',
1310
+ stack: event.reason?.stack,
1311
+ },
1312
+ breadcrumbs: breadcrumbStore.getAll(),
1313
+ timestamp: new Date().toISOString()
1314
+ };
1315
+
1316
+ this.handleError(errorPayload);
1317
+
1318
+ // ✅ CHAINING: Llamar al handler original si existe
1319
+ if (this.originalHandlers.onunhandledrejection) {
1320
+ try {
1321
+ this.originalHandlers.onunhandledrejection(event);
1322
+ } catch (originalError) {
1323
+ console.warn('SyntropyFront: Error en handler original de rejection:', originalError);
1324
+ }
1325
+ }
1326
+ };
1327
+
1328
+ // Sobrescribir con el nuevo handler
1329
+ window.onunhandledrejection = syntropyRejectionHandler;
1330
+ }
1331
+ }
1332
+
1333
+ /**
1334
+ * Maneja los errores capturados
1335
+ * @param {Object} errorPayload - Payload del error
1336
+ */
1337
+ handleError(errorPayload) {
1338
+ // Recolectar contexto si está configurado
1339
+ const context = this.contextTypes.length > 0 ? contextCollector.collect(this.contextTypes) : null;
1340
+
1341
+ // Enviar al agent si está configurado
1342
+ agent.sendError(errorPayload, context);
1343
+
1344
+ // Callback para manejo personalizado de errores
1345
+ if (this.onError) {
1346
+ this.onError(errorPayload);
1347
+ } else {
1348
+ // Comportamiento por defecto: log a consola
1349
+ console.error('SyntropyFront - Error detectado:', errorPayload);
1350
+ }
1351
+ }
1352
+
1353
+ /**
1354
+ * Desactiva todos los interceptores y restaura handlers originales
1355
+ */
1356
+ destroy() {
1357
+ if (!this.isInitialized) return;
1358
+
1359
+ console.log('SyntropyFront: Limpiando interceptores...');
1360
+
1361
+ // ✅ RESTAURAR: Handlers originales
1362
+ if (this.originalHandlers.fetch) {
1363
+ window.fetch = this.originalHandlers.fetch;
1364
+ console.log('SyntropyFront: fetch original restaurado');
1365
+ }
1366
+
1367
+ if (this.originalHandlers.onerror) {
1368
+ window.onerror = this.originalHandlers.onerror;
1369
+ console.log('SyntropyFront: onerror original restaurado');
1370
+ }
1371
+
1372
+ if (this.originalHandlers.onunhandledrejection) {
1373
+ window.onunhandledrejection = this.originalHandlers.onunhandledrejection;
1374
+ console.log('SyntropyFront: onunhandledrejection original restaurado');
1375
+ }
1376
+
1377
+ // ✅ LIMPIAR: Event listeners
1378
+ this.eventListeners.forEach((handler, eventType) => {
1379
+ document.removeEventListener(eventType, handler, true);
1380
+ console.log(`SyntropyFront: Event listener ${eventType} removido`);
1381
+ });
1382
+
1383
+ // Limpiar referencias
1384
+ this.originalHandlers = {
1385
+ fetch: null,
1386
+ onerror: null,
1387
+ onunhandledrejection: null
1388
+ };
1389
+ this.eventListeners.clear();
1390
+ this.isInitialized = false;
1391
+
1392
+ console.log('SyntropyFront: Interceptors destruidos y handlers restaurados');
1393
+ }
1394
+
1395
+ /**
1396
+ * Obtiene información sobre los handlers originales
1397
+ * @returns {Object} Información de handlers
1398
+ */
1399
+ getHandlerInfo() {
1400
+ return {
1401
+ isInitialized: this.isInitialized,
1402
+ hasOriginalFetch: !!this.originalHandlers.fetch,
1403
+ hasOriginalOnError: !!this.originalHandlers.onerror,
1404
+ hasOriginalOnUnhandledRejection: !!this.originalHandlers.onunhandledrejection,
1405
+ eventListenersCount: this.eventListeners.size
1406
+ };
1407
+ }
1408
+ }
1409
+
1410
+ // Instancia singleton
1411
+ const interceptors = new Interceptors();
1412
+
1413
+ /**
1414
+ * Presets de configuración para SyntropyFront
1415
+ * Recetas pre-configuradas para diferentes casos de uso
1416
+ *
1417
+ * @author SyntropyFront Team
1418
+ * @version 1.0.0
1419
+ */
1420
+
1421
+ /**
1422
+ * Preset 'safe' - Modo solo emergencias
1423
+ * Ideal para: Producción, aplicaciones críticas, GDPR estricto
1424
+ */
1425
+ const SAFE_PRESET = {
1426
+ name: 'safe',
1427
+ description: 'Modo solo emergencias - Mínimo impacto, máxima privacidad',
1428
+
1429
+ // Configuración del agent
1430
+ agent: {
1431
+ batchTimeout: null, // Solo errores
1432
+ batchSize: 5,
1433
+ encrypt: null // Sin encriptación por defecto
1434
+ },
1435
+
1436
+ // Breadcrumbs limitados
1437
+ maxBreadcrumbs: 10,
1438
+
1439
+ // Contexto mínimo
1440
+ context: {
1441
+ device: true, // Solo información básica del dispositivo
1442
+ window: false, // No URL ni viewport
1443
+ session: true, // Solo sessionId
1444
+ ui: false, // No información de UI
1445
+ network: false // No información de red
1446
+ },
1447
+
1448
+ // Sin tracking de objetos
1449
+ customObjects: {},
1450
+ proxyTracking: false,
1451
+
1452
+ // Interceptores básicos
1453
+ captureClicks: false,
1454
+ captureFetch: false,
1455
+ captureErrors: true,
1456
+ captureUnhandledRejections: true,
1457
+
1458
+ // Worker opcional
1459
+ useWorker: false,
1460
+
1461
+ // Callbacks
1462
+ onError: null,
1463
+ onBreadcrumbAdded: null
1464
+ };
1465
+
1466
+ /**
1467
+ * Preset 'balanced' - Modo equilibrado
1468
+ * Ideal para: Desarrollo, testing, aplicaciones generales
1469
+ */
1470
+ const BALANCED_PRESET = {
1471
+ name: 'balanced',
1472
+ description: 'Modo equilibrado - Balance entre información y performance',
1473
+
1474
+ // Configuración del agent
1475
+ agent: {
1476
+ batchTimeout: 10000, // Envío cada 10 segundos
1477
+ batchSize: 20,
1478
+ encrypt: null
1479
+ },
1480
+
1481
+ // Breadcrumbs moderados
1482
+ maxBreadcrumbs: 50,
1483
+
1484
+ // Contexto curado
1485
+ context: {
1486
+ device: true, // Información completa del dispositivo
1487
+ window: true, // URL y viewport
1488
+ session: true, // Información de sesión
1489
+ ui: true, // Estado básico de UI
1490
+ network: true // Estado de conectividad
1491
+ },
1492
+
1493
+ // Tracking de objetos moderado
1494
+ customObjects: {},
1495
+ proxyTracking: {
1496
+ enabled: true,
1497
+ maxStates: 10,
1498
+ trackNested: true,
1499
+ trackArrays: false
1500
+ },
1501
+
1502
+ // Interceptores completos
1503
+ captureClicks: true,
1504
+ captureFetch: true,
1505
+ captureErrors: true,
1506
+ captureUnhandledRejections: true,
1507
+
1508
+ // Worker habilitado
1509
+ useWorker: true,
1510
+
1511
+ // Callbacks
1512
+ onError: null,
1513
+ onBreadcrumbAdded: null
1514
+ };
1515
+
1516
+ /**
1517
+ * Preset 'debug' - Modo debug completo
1518
+ * Ideal para: Desarrollo, debugging, análisis profundo
1519
+ */
1520
+ const DEBUG_PRESET = {
1521
+ name: 'debug',
1522
+ description: 'Modo debug completo - Máxima información para desarrollo',
1523
+
1524
+ // Configuración del agent
1525
+ agent: {
1526
+ batchTimeout: 5000, // Envío cada 5 segundos
1527
+ batchSize: 50,
1528
+ encrypt: null
1529
+ },
1530
+
1531
+ // Breadcrumbs completos
1532
+ maxBreadcrumbs: 100,
1533
+
1534
+ // Contexto completo
1535
+ context: {
1536
+ device: true, // Todo del dispositivo
1537
+ window: true, // Todo de la ventana
1538
+ session: true, // Todo de la sesión
1539
+ ui: true, // Todo de la UI
1540
+ network: true // Todo de la red
1541
+ },
1542
+
1543
+ // Tracking de objetos completo
1544
+ customObjects: {},
1545
+ proxyTracking: {
1546
+ enabled: true,
1547
+ maxStates: 20,
1548
+ trackNested: true,
1549
+ trackArrays: true,
1550
+ trackFunctions: true
1551
+ },
1552
+
1553
+ // Todos los interceptores
1554
+ captureClicks: true,
1555
+ captureFetch: true,
1556
+ captureErrors: true,
1557
+ captureUnhandledRejections: true,
1558
+
1559
+ // Worker habilitado
1560
+ useWorker: true,
1561
+
1562
+ // Callbacks para debugging
1563
+ onError: (error) => {
1564
+ console.error('SyntropyFront Error:', error);
1565
+ },
1566
+ onBreadcrumbAdded: (breadcrumb) => {
1567
+ console.log('SyntropyFront Breadcrumb:', breadcrumb);
1568
+ }
1569
+ };
1570
+
1571
+ /**
1572
+ * Preset 'performance' - Modo optimizado para performance
1573
+ * Ideal para: Aplicaciones de alta performance, gaming, real-time
1574
+ */
1575
+ const PERFORMANCE_PRESET = {
1576
+ name: 'performance',
1577
+ description: 'Modo performance - Máxima velocidad, información mínima',
1578
+
1579
+ // Configuración del agent
1580
+ agent: {
1581
+ batchTimeout: null, // Solo errores críticos
1582
+ batchSize: 3,
1583
+ encrypt: null
1584
+ },
1585
+
1586
+ // Breadcrumbs mínimos
1587
+ maxBreadcrumbs: 5,
1588
+
1589
+ // Contexto mínimo
1590
+ context: {
1591
+ device: false, // Sin información de dispositivo
1592
+ window: false, // Sin información de ventana
1593
+ session: true, // Solo sessionId
1594
+ ui: false, // Sin información de UI
1595
+ network: false // Sin información de red
1596
+ },
1597
+
1598
+ // Sin tracking de objetos
1599
+ customObjects: {},
1600
+ proxyTracking: false,
1601
+
1602
+ // Solo errores críticos
1603
+ captureClicks: false,
1604
+ captureFetch: false,
1605
+ captureErrors: true,
1606
+ captureUnhandledRejections: true,
1607
+
1608
+ // Sin worker para máxima velocidad
1609
+ useWorker: false,
1610
+
1611
+ // Sin callbacks
1612
+ onError: null,
1613
+ onBreadcrumbAdded: null
1614
+ };
1615
+
1616
+ /**
1617
+ * Mapa de presets disponibles
1618
+ */
1619
+ const PRESETS = {
1620
+ safe: SAFE_PRESET,
1621
+ balanced: BALANCED_PRESET,
1622
+ debug: DEBUG_PRESET,
1623
+ performance: PERFORMANCE_PRESET
1624
+ };
1625
+
1626
+ /**
1627
+ * Obtiene un preset por nombre
1628
+ */
1629
+ function getPreset(name) {
1630
+ const preset = PRESETS[name];
1631
+ if (!preset) {
1632
+ throw new Error(`Preset '${name}' no encontrado. Presets disponibles: ${Object.keys(PRESETS).join(', ')}`);
1633
+ }
1634
+ return preset;
1635
+ }
1636
+
1637
+ /**
1638
+ * Lista todos los presets disponibles
1639
+ */
1640
+ function getAvailablePresets() {
1641
+ return Object.keys(PRESETS).map(name => ({
1642
+ name,
1643
+ ...PRESETS[name]
1644
+ }));
1645
+ }
1646
+
1647
+ /**
1648
+ * Obtiene información de un preset
1649
+ */
1650
+ function getPresetInfo(name) {
1651
+ const preset = getPreset(name);
1652
+ return {
1653
+ name: preset.name,
1654
+ description: preset.description,
1655
+ features: {
1656
+ breadcrumbs: preset.maxBreadcrumbs,
1657
+ context: Object.keys(preset.context).filter(key => preset.context[key]).length,
1658
+ worker: preset.useWorker,
1659
+ proxyTracking: preset.proxyTracking?.enabled || false,
1660
+ agentMode: preset.agent.batchTimeout ? 'completo' : 'solo emergencias'
1661
+ }
1662
+ };
1663
+ }
1664
+
1665
+ /**
1666
+ * SyntropyFront - Sistema de trazabilidad para frontend
1667
+ * API principal para inicializar y configurar el sistema
1668
+ */
1669
+ class SyntropyFront {
1670
+ constructor() {
1671
+ this.isInitialized = false;
1672
+ this.currentPreset = null;
1673
+
1674
+ // Lazy-loaded modules
1675
+ this.proxyObjectTracker = null;
1676
+ this.interceptorRegistry = null;
1677
+ this.workerManager = null;
1678
+
1679
+ // Configuración por defecto (balanced preset)
1680
+ this.config = {
1681
+ preset: 'balanced', // Preset por defecto
1682
+ maxBreadcrumbs: 50,
1683
+ captureClicks: true,
1684
+ captureFetch: true,
1685
+ captureErrors: true,
1686
+ captureUnhandledRejections: true,
1687
+ onError: null,
1688
+ onBreadcrumbAdded: null,
1689
+ // Configuración del agent
1690
+ agent: {
1691
+ endpoint: null,
1692
+ headers: {},
1693
+ batchSize: 20,
1694
+ batchTimeout: 10000, // 10 segundos por defecto
1695
+ encrypt: null // Callback de encriptación opcional
1696
+ },
1697
+ // Configuración de contexto (nueva arquitectura granular)
1698
+ context: {
1699
+ device: true, // Set curado por defecto
1700
+ window: true, // Set curado por defecto
1701
+ session: true, // Set curado por defecto
1702
+ ui: true, // Set curado por defecto
1703
+ network: true // Set curado por defecto
1704
+ },
1705
+ // Configuración de proxy tracking
1706
+ proxyTracking: {
1707
+ enabled: true,
1708
+ maxStates: 10,
1709
+ trackNested: true,
1710
+ trackArrays: false
1711
+ },
1712
+ // Worker habilitado por defecto
1713
+ useWorker: true
1714
+ };
1715
+ }
1716
+
1717
+ /**
1718
+ * Inicializa el sistema de trazabilidad
1719
+ * @param {Object} options - Opciones de configuración
1720
+ */
1721
+ async init(options = {}) {
1722
+ if (this.isInitialized) {
1723
+ console.warn('SyntropyFront ya está inicializado');
1724
+ return;
1725
+ }
1726
+
1727
+ // Manejar preset si se especifica
1728
+ if (options.preset) {
1729
+ try {
1730
+ const preset = getPreset(options.preset);
1731
+ this.currentPreset = options.preset;
1732
+
1733
+ // Aplicar configuración del preset
1734
+ this.config = { ...this.config, ...preset };
1735
+
1736
+ console.log(`🎯 SyntropyFront: Aplicando preset '${options.preset}' - ${preset.description}`);
1737
+ } catch (error) {
1738
+ console.error(`❌ SyntropyFront: Error aplicando preset '${options.preset}':`, error.message);
1739
+ throw error;
1740
+ }
1741
+ }
1742
+
1743
+ // Aplicar configuración personalizada (sobrescribe preset)
1744
+ this.config = { ...this.config, ...options };
1745
+
1746
+ // Configurar agent primero
1747
+ if (this.config.agent.endpoint) {
1748
+ agent.configure(this.config.agent);
1749
+ }
1750
+
1751
+ // Configurar worker manager
1752
+ if (this.config.useWorker !== false) {
1753
+ await this.workerManager.init({
1754
+ maxBreadcrumbs: this.config.maxBreadcrumbs,
1755
+ agent: this.config.agent
1756
+ });
1757
+ }
1758
+
1759
+ // Configurar breadcrumb store
1760
+ breadcrumbStore.setMaxBreadcrumbs(this.config.maxBreadcrumbs);
1761
+ breadcrumbStore.onBreadcrumbAdded = this.config.onBreadcrumbAdded;
1762
+ breadcrumbStore.setAgent(agent);
1763
+
1764
+ // Configurar contexto (nueva arquitectura granular)
1765
+ this.contextConfig = this.config.context || {
1766
+ device: true,
1767
+ window: true,
1768
+ session: true,
1769
+ ui: true,
1770
+ network: true
1771
+ };
1772
+
1773
+
1774
+
1775
+ // Lazy load modules based on configuration
1776
+ await this.loadModules();
1777
+
1778
+ // Configurar proxy object tracker (si está habilitado)
1779
+ if (this.config.proxyTracking?.enabled && this.proxyObjectTracker) {
1780
+ this.proxyObjectTracker.configure(this.config.proxyTracking);
1781
+ }
1782
+
1783
+ // Configurar interceptores
1784
+ interceptors.configure({
1785
+ captureClicks: this.config.captureClicks,
1786
+ captureFetch: this.config.captureFetch,
1787
+ captureErrors: this.config.captureErrors,
1788
+ captureUnhandledRejections: this.config.captureUnhandledRejections
1789
+ });
1790
+
1791
+ interceptors.onError = this.config.onError;
1792
+
1793
+ // Inicializar interceptores
1794
+ interceptors.init();
1795
+
1796
+ // Inicializar interceptores personalizados (si están habilitados)
1797
+ if (this.interceptorRegistry) {
1798
+ this.interceptorRegistry.init({
1799
+ breadcrumbStore,
1800
+ agent,
1801
+ contextCollector
1802
+ });
1803
+ }
1804
+
1805
+ this.isInitialized = true;
1806
+ console.log('SyntropyFront inicializado correctamente');
1807
+ }
1808
+
1809
+ /**
1810
+ * Carga módulos dinámicamente basado en la configuración
1811
+ */
1812
+ async loadModules() {
1813
+ const loadPromises = [];
1814
+
1815
+ // Cargar ProxyObjectTracker si está habilitado
1816
+ if (this.config.proxyTracking?.enabled) {
1817
+ loadPromises.push(
1818
+ Promise.resolve().then(function () { return ProxyObjectTracker$1; })
1819
+ .then(module => {
1820
+ this.proxyObjectTracker = module.proxyObjectTracker;
1821
+ console.log('🔄 ProxyObjectTracker cargado dinámicamente');
1822
+ })
1823
+ .catch(error => {
1824
+ console.warn('⚠️ Error cargando ProxyObjectTracker:', error);
1825
+ })
1826
+ );
1827
+ }
1828
+
1829
+ // Cargar InterceptorRegistry si hay interceptores personalizados
1830
+ if (this.config.useInterceptors !== false) {
1831
+ loadPromises.push(
1832
+ Promise.resolve().then(function () { return InterceptorRegistry$1; })
1833
+ .then(module => {
1834
+ this.interceptorRegistry = module.interceptorRegistry;
1835
+ console.log('🔄 InterceptorRegistry cargado dinámicamente');
1836
+ })
1837
+ .catch(error => {
1838
+ console.warn('⚠️ Error cargando InterceptorRegistry:', error);
1839
+ })
1840
+ );
1841
+ }
1842
+
1843
+ // Cargar WorkerManager si está habilitado
1844
+ if (this.config.useWorker !== false) {
1845
+ loadPromises.push(
1846
+ Promise.resolve().then(function () { return WorkerManager$1; })
1847
+ .then(module => {
1848
+ this.workerManager = new module.default();
1849
+ console.log('🔄 WorkerManager cargado dinámicamente');
1850
+ })
1851
+ .catch(error => {
1852
+ console.warn('⚠️ Error cargando WorkerManager:', error);
1853
+ })
1854
+ );
1855
+ }
1856
+
1857
+ // Esperar a que todos los módulos se carguen
1858
+ await Promise.all(loadPromises);
1859
+ }
1860
+
1861
+ /**
1862
+ * Añade un breadcrumb manualmente
1863
+ * @param {string} category - Categoría del evento
1864
+ * @param {string} message - Mensaje descriptivo
1865
+ * @param {Object} data - Datos adicionales
1866
+ */
1867
+ addBreadcrumb(category, message, data = {}) {
1868
+ breadcrumbStore.add({ category, message, data });
1869
+ }
1870
+
1871
+ /**
1872
+ * Obtiene todos los breadcrumbs
1873
+ * @returns {Array} Lista de breadcrumbs
1874
+ */
1875
+ getBreadcrumbs() {
1876
+ return breadcrumbStore.getAll();
1877
+ }
1878
+
1879
+ /**
1880
+ * Obtiene breadcrumbs por categoría
1881
+ * @param {string} category - Categoría a filtrar
1882
+ * @returns {Array} Breadcrumbs de la categoría
1883
+ */
1884
+ getBreadcrumbsByCategory(category) {
1885
+ return breadcrumbStore.getByCategory(category);
1886
+ }
1887
+
1888
+ /**
1889
+ * Limpia todos los breadcrumbs
1890
+ */
1891
+ clearBreadcrumbs() {
1892
+ breadcrumbStore.clear();
1893
+ }
1894
+
1895
+ /**
1896
+ * Desactiva el sistema de trazabilidad
1897
+ */
1898
+ destroy() {
1899
+ if (!this.isInitialized) return;
1900
+
1901
+ interceptors.destroy();
1902
+
1903
+ if (this.interceptorRegistry) {
1904
+ this.interceptorRegistry.destroy();
1905
+ }
1906
+
1907
+ breadcrumbStore.clear();
1908
+ agent.disable();
1909
+
1910
+ if (this.workerManager) {
1911
+ this.workerManager.destroy();
1912
+ }
1913
+
1914
+ this.isInitialized = false;
1915
+ console.log('SyntropyFront desactivado');
1916
+ }
1917
+
1918
+ /**
1919
+ * Configura el tamaño máximo de breadcrumbs
1920
+ * @param {number} maxBreadcrumbs - Nuevo tamaño máximo
1921
+ */
1922
+ setMaxBreadcrumbs(maxBreadcrumbs) {
1923
+ breadcrumbStore.setMaxBreadcrumbs(maxBreadcrumbs);
1924
+ }
1925
+
1926
+ /**
1927
+ * Obtiene el tamaño máximo actual de breadcrumbs
1928
+ * @returns {number} Tamaño máximo
1929
+ */
1930
+ getMaxBreadcrumbs() {
1931
+ return breadcrumbStore.getMaxBreadcrumbs();
1932
+ }
1933
+
1934
+ /**
1935
+ * Fuerza el envío de datos pendientes al backend
1936
+ */
1937
+ async flush() {
1938
+ await agent.forceFlush();
1939
+ }
1940
+
1941
+ /**
1942
+ * Obtiene el contexto actual según la configuración
1943
+ * @returns {Object} Contexto recolectado
1944
+ */
1945
+ getContext() {
1946
+ const context = contextCollector.collect(this.contextConfig);
1947
+
1948
+ // Agregar objetos personalizados
1949
+ const customObjects = customObjectCollector.collectCustomObjects();
1950
+ if (Object.keys(customObjects).length > 0) {
1951
+ context.customObjects = customObjects;
1952
+ }
1953
+
1954
+ return context;
1955
+ }
1956
+
1957
+ /**
1958
+ * Obtiene todos los tipos de contexto disponibles
1959
+ * @returns {Array} Tipos disponibles
1960
+ */
1961
+ getAvailableContextTypes() {
1962
+ return contextCollector.getAvailableTypes();
1963
+ }
1964
+
1965
+ /**
1966
+ * Obtiene los campos disponibles para un tipo de contexto
1967
+ * @param {string} contextType - Tipo de contexto
1968
+ * @returns {Array} Campos disponibles
1969
+ */
1970
+ getAvailableContextFields(contextType) {
1971
+ return contextCollector.getAvailableFields(contextType);
1972
+ }
1973
+
1974
+ /**
1975
+ * Obtiene información sobre los sets por defecto
1976
+ * @returns {Object} Información de sets por defecto
1977
+ */
1978
+ getDefaultContextsInfo() {
1979
+ return contextCollector.getDefaultContextsInfo();
1980
+ }
1981
+
1982
+ /**
1983
+ * Configura el contexto a recolectar
1984
+ * @param {Object} contextConfig - Configuración de contexto
1985
+ */
1986
+ setContext(contextConfig) {
1987
+ if (typeof contextConfig !== 'object') {
1988
+ console.warn('SyntropyFront: contextConfig debe ser un objeto');
1989
+ return;
1990
+ }
1991
+
1992
+ this.contextConfig = contextConfig;
1993
+ console.log('SyntropyFront: Configuración de contexto actualizada:', contextConfig);
1994
+ }
1995
+
1996
+ /**
1997
+ * Configura los tipos de contexto a recolectar (método legacy)
1998
+ * @param {Array} contextTypes - Tipos de contexto
1999
+ */
2000
+ setContextTypes(contextTypes) {
2001
+ if (!Array.isArray(contextTypes)) {
2002
+ console.warn('SyntropyFront: contextTypes debe ser un array');
2003
+ return;
2004
+ }
2005
+
2006
+ // Convertir array a configuración por defecto
2007
+ const contextConfig = {};
2008
+ contextTypes.forEach(type => {
2009
+ contextConfig[type] = true; // Usar set por defecto
2010
+ });
2011
+
2012
+ this.setContext(contextConfig);
2013
+ }
2014
+
2015
+ // ===== DEPRECATED: CUSTOM OBJECT METHODS =====
2016
+ // Estos métodos están deprecados. Usa ProxyObjectTracker en su lugar.
2017
+
2018
+ /**
2019
+ * @deprecated Usa addProxyObject() en su lugar
2020
+ */
2021
+ addCustomObject(name, source, maxStates = 10) {
2022
+ console.warn('SyntropyFront: addCustomObject() está deprecado. Usa addProxyObject() en su lugar.');
2023
+ throw new Error('addCustomObject() está deprecado. Usa addProxyObject() en su lugar.');
2024
+ }
2025
+
2026
+ /**
2027
+ * @deprecated Usa removeProxyObject() en su lugar
2028
+ */
2029
+ removeCustomObject(name) {
2030
+ console.warn('SyntropyFront: removeCustomObject() está deprecado. Usa removeProxyObject() en su lugar.');
2031
+ throw new Error('removeCustomObject() está deprecado. Usa removeProxyObject() en su lugar.');
2032
+ }
2033
+
2034
+ /**
2035
+ * @deprecated Usa getProxyObjectState() en su lugar
2036
+ */
2037
+ getCustomObjectValue(name) {
2038
+ console.warn('SyntropyFront: getCustomObjectValue() está deprecado. Usa getProxyObjectState() en su lugar.');
2039
+ throw new Error('getCustomObjectValue() está deprecado. Usa getProxyObjectState() en su lugar.');
2040
+ }
2041
+
2042
+ /**
2043
+ * @deprecated Usa getProxyObjectHistory() en su lugar
2044
+ */
2045
+ getCustomObjectHistory(name) {
2046
+ console.warn('SyntropyFront: getCustomObjectHistory() está deprecado. Usa getProxyObjectHistory() en su lugar.');
2047
+ throw new Error('getCustomObjectHistory() está deprecado. Usa getProxyObjectHistory() en su lugar.');
2048
+ }
2049
+
2050
+ /**
2051
+ * @deprecated Usa getProxyTrackedObjects() en su lugar
2052
+ */
2053
+ getCustomObjectNames() {
2054
+ console.warn('SyntropyFront: getCustomObjectNames() está deprecado. Usa getProxyTrackedObjects() en su lugar.');
2055
+ throw new Error('getCustomObjectNames() está deprecado. Usa getProxyTrackedObjects() en su lugar.');
2056
+ }
2057
+
2058
+ /**
2059
+ * Inyecta un interceptor personalizado
2060
+ * @param {string} name - Nombre del interceptor
2061
+ * @param {Object} interceptor - Objeto interceptor con métodos init/destroy
2062
+ * @returns {SyntropyFront} Instancia para chaining
2063
+ */
2064
+ inject(name, interceptor) {
2065
+ if (!this.interceptorRegistry) {
2066
+ console.warn('SyntropyFront: InterceptorRegistry no está cargado. Asegúrate de que useInterceptors no esté en false.');
2067
+ return this;
2068
+ }
2069
+ this.interceptorRegistry.register(name, interceptor);
2070
+ return this; // Para chaining
2071
+ }
2072
+
2073
+ /**
2074
+ * Remueve un interceptor personalizado
2075
+ * @param {string} name - Nombre del interceptor
2076
+ */
2077
+ removeInterceptor(name) {
2078
+ if (!this.interceptorRegistry) {
2079
+ console.warn('SyntropyFront: InterceptorRegistry no está cargado.');
2080
+ return;
2081
+ }
2082
+ this.interceptorRegistry.unregister(name);
2083
+ }
2084
+
2085
+ /**
2086
+ * Obtiene la lista de interceptores registrados
2087
+ * @returns {Array} Lista de nombres de interceptores
2088
+ */
2089
+ getRegisteredInterceptors() {
2090
+ if (!this.interceptorRegistry) {
2091
+ return [];
2092
+ }
2093
+ return this.interceptorRegistry.getRegisteredInterceptors();
2094
+ }
2095
+
2096
+ /**
2097
+ * Obtiene información de un interceptor específico
2098
+ * @param {string} name - Nombre del interceptor
2099
+ * @returns {Object|null} Información del interceptor
2100
+ */
2101
+ getInterceptorInfo(name) {
2102
+ if (!this.interceptorRegistry) {
2103
+ return null;
2104
+ }
2105
+ return this.interceptorRegistry.getInterceptorInfo(name);
2106
+ }
2107
+
2108
+ /**
2109
+ * Verifica si está inicializado
2110
+ * @returns {boolean} Estado de inicialización
2111
+ */
2112
+ isActive() {
2113
+ return this.isInitialized;
2114
+ }
2115
+
2116
+ // ===== PROXY OBJECT TRACKER METHODS =====
2117
+
2118
+ /**
2119
+ * Agrega un objeto para tracking reactivo con Proxy
2120
+ * @param {string} objectPath - Ruta/nombre del objeto
2121
+ * @param {Object} targetObject - Objeto a trackear
2122
+ * @param {Object} options - Opciones de tracking
2123
+ * @returns {Object} Proxy del objeto original
2124
+ */
2125
+ addProxyObject(objectPath, targetObject, options = {}) {
2126
+ if (!this.proxyObjectTracker) {
2127
+ console.warn('SyntropyFront: ProxyObjectTracker no está cargado. Asegúrate de que proxyTracking.enabled esté en true.');
2128
+ return targetObject;
2129
+ }
2130
+ return this.proxyObjectTracker.addObject(objectPath, targetObject, options);
2131
+ }
2132
+
2133
+ /**
2134
+ * Obtiene el historial de estados de un objeto trackeado
2135
+ * @param {string} objectPath - Ruta del objeto
2136
+ * @returns {Array} Historial de estados
2137
+ */
2138
+ getProxyObjectHistory(objectPath) {
2139
+ if (!this.proxyObjectTracker) {
2140
+ return [];
2141
+ }
2142
+ return this.proxyObjectTracker.getObjectHistory(objectPath);
2143
+ }
2144
+
2145
+ /**
2146
+ * Obtiene el estado actual de un objeto trackeado
2147
+ * @param {string} objectPath - Ruta del objeto
2148
+ * @returns {Object|null} Estado actual
2149
+ */
2150
+ getProxyObjectState(objectPath) {
2151
+ if (!this.proxyObjectTracker) {
2152
+ return null;
2153
+ }
2154
+ return this.proxyObjectTracker.getCurrentState(objectPath);
2155
+ }
2156
+
2157
+ /**
2158
+ * Obtiene todos los objetos trackeados con Proxy
2159
+ * @returns {Array} Lista de objetos trackeados
2160
+ */
2161
+ getProxyTrackedObjects() {
2162
+ if (!this.proxyObjectTracker) {
2163
+ return [];
2164
+ }
2165
+ return this.proxyObjectTracker.getTrackedObjects();
2166
+ }
2167
+
2168
+ /**
2169
+ * Remueve un objeto del tracking con Proxy
2170
+ * @param {string} objectPath - Ruta del objeto
2171
+ * @returns {Object|null} Objeto original (sin proxy)
2172
+ */
2173
+ removeProxyObject(objectPath) {
2174
+ if (!this.proxyObjectTracker) {
2175
+ return null;
2176
+ }
2177
+ return this.proxyObjectTracker.removeObject(objectPath);
2178
+ }
2179
+
2180
+ /**
2181
+ * Limpia todos los objetos trackeados con Proxy
2182
+ */
2183
+ clearProxyObjects() {
2184
+ if (!this.proxyObjectTracker) {
2185
+ return;
2186
+ }
2187
+ this.proxyObjectTracker.clear();
2188
+ }
2189
+
2190
+ /**
2191
+ * Obtiene estadísticas del ProxyObjectTracker
2192
+ * @returns {Object} Estadísticas
2193
+ */
2194
+ getProxyTrackerStats() {
2195
+ if (!this.proxyObjectTracker) {
2196
+ return { enabled: false, trackedObjects: 0 };
2197
+ }
2198
+ return this.proxyObjectTracker.getStats();
2199
+ }
2200
+
2201
+ // Worker Manager Methods
2202
+ async addBreadcrumbToWorker(type, message, data = {}) {
2203
+ if (!this.workerManager) {
2204
+ console.warn('SyntropyFront: WorkerManager no está cargado. Asegúrate de que useWorker no esté en false.');
2205
+ return this.addBreadcrumb(type, message, data);
2206
+ }
2207
+ if (this.workerManager.isWorkerAvailable()) {
2208
+ return await this.workerManager.addBreadcrumb(type, message, data);
2209
+ } else {
2210
+ // Fallback al método normal
2211
+ return this.addBreadcrumb(type, message, data);
2212
+ }
2213
+ }
2214
+
2215
+ async getBreadcrumbsFromWorker() {
2216
+ if (!this.workerManager) {
2217
+ return this.getBreadcrumbs();
2218
+ }
2219
+ if (this.workerManager.isWorkerAvailable()) {
2220
+ return await this.workerManager.getBreadcrumbs();
2221
+ } else {
2222
+ return this.getBreadcrumbs();
2223
+ }
2224
+ }
2225
+
2226
+ async clearBreadcrumbsFromWorker() {
2227
+ if (!this.workerManager) {
2228
+ return this.clearBreadcrumbs();
2229
+ }
2230
+ if (this.workerManager.isWorkerAvailable()) {
2231
+ return await this.workerManager.clearBreadcrumbs();
2232
+ } else {
2233
+ return this.clearBreadcrumbs();
2234
+ }
2235
+ }
2236
+
2237
+ async sendErrorToWorker(error, context = {}) {
2238
+ if (!this.workerManager) {
2239
+ console.warn('SyntropyFront: WorkerManager no está cargado. Asegúrate de que useWorker no esté en false.');
2240
+ return this.sendError(error, context);
2241
+ }
2242
+ if (this.workerManager.isWorkerAvailable()) {
2243
+ return await this.workerManager.sendError(error, context);
2244
+ } else {
2245
+ // Fallback al método normal
2246
+ return this.sendError(error, context);
2247
+ }
2248
+ }
2249
+
2250
+ async pingWorker() {
2251
+ if (!this.workerManager) {
2252
+ return { success: false, message: 'Worker no cargado' };
2253
+ }
2254
+ if (this.workerManager.isWorkerAvailable()) {
2255
+ return await this.workerManager.ping();
2256
+ } else {
2257
+ return { success: false, message: 'Worker no disponible' };
2258
+ }
2259
+ }
2260
+
2261
+ getWorkerStatus() {
2262
+ if (!this.workerManager) {
2263
+ return { isAvailable: false, isInitialized: false, pendingRequests: 0 };
2264
+ }
2265
+ return this.workerManager.getStatus();
2266
+ }
2267
+
2268
+ isWorkerAvailable() {
2269
+ if (!this.workerManager) {
2270
+ return false;
2271
+ }
2272
+ return this.workerManager.isWorkerAvailable();
2273
+ }
2274
+
2275
+ // Preset Methods
2276
+ getCurrentPreset() {
2277
+ return this.currentPreset;
2278
+ }
2279
+
2280
+ getPresetInfo(presetName = null) {
2281
+ const name = presetName || this.currentPreset;
2282
+ if (!name) {
2283
+ return null;
2284
+ }
2285
+ return getPresetInfo(name);
2286
+ }
2287
+
2288
+ getAvailablePresets() {
2289
+ return getAvailablePresets();
2290
+ }
2291
+
2292
+ async changePreset(presetName, options = {}) {
2293
+ if (this.isInitialized) {
2294
+ console.warn('SyntropyFront: No se puede cambiar preset después de la inicialización');
2295
+ return false;
2296
+ }
2297
+
2298
+ try {
2299
+ const preset = getPreset(presetName);
2300
+ this.currentPreset = presetName;
2301
+
2302
+ // Aplicar preset
2303
+ this.config = { ...this.config, ...preset };
2304
+
2305
+ // Aplicar opciones adicionales
2306
+ this.config = { ...this.config, ...options };
2307
+
2308
+ console.log(`🎯 SyntropyFront: Preset cambiado a '${presetName}' - ${preset.description}`);
2309
+ return true;
2310
+ } catch (error) {
2311
+ console.error(`❌ SyntropyFront: Error cambiando preset a '${presetName}':`, error.message);
2312
+ return false;
2313
+ }
2314
+ }
2315
+
2316
+ getConfiguration() {
2317
+ return {
2318
+ currentPreset: this.currentPreset,
2319
+ config: this.config,
2320
+ isInitialized: this.isInitialized
2321
+ };
2322
+ }
2323
+ }
2324
+
2325
+ // Instancia singleton principal
2326
+ const syntropyFront = new SyntropyFront();
2327
+
2328
+ /**
2329
+ * ProxyObjectTracker - Tracking reactivo de objetos usando Proxy
2330
+ * Captura cambios en tiempo real sin necesidad de polling
2331
+ */
2332
+ class ProxyObjectTracker {
2333
+ constructor() {
2334
+ this.trackedObjects = new Map(); // Map<objectPath, ProxyInfo>
2335
+ this.maxStates = 10; // Estados máximos por objeto
2336
+ this.isEnabled = true;
2337
+ this.onChangeCallback = null; // Callback cuando cambia un objeto
2338
+ }
2339
+
2340
+ /**
2341
+ * Configura el tracker
2342
+ * @param {Object} config - Configuración
2343
+ * @param {number} [config.maxStates] - Máximo número de estados por objeto
2344
+ * @param {Function} [config.onChange] - Callback cuando cambia un objeto
2345
+ */
2346
+ configure(config = {}) {
2347
+ this.maxStates = config.maxStates || this.maxStates;
2348
+ this.onChangeCallback = config.onChange || null;
2349
+ this.isEnabled = config.enabled !== false;
2350
+ }
2351
+
2352
+ /**
2353
+ * Agrega un objeto para tracking reactivo
2354
+ * @param {string} objectPath - Ruta/nombre del objeto
2355
+ * @param {Object} targetObject - Objeto a trackear
2356
+ * @param {Object} options - Opciones de tracking
2357
+ * @returns {Object} Proxy del objeto original
2358
+ */
2359
+ addObject(objectPath, targetObject, options = {}) {
2360
+ if (!this.isEnabled) {
2361
+ console.warn('SyntropyFront: ProxyObjectTracker deshabilitado');
2362
+ return targetObject;
2363
+ }
2364
+
2365
+ if (!targetObject || typeof targetObject !== 'object') {
2366
+ console.warn(`SyntropyFront: Objeto inválido para tracking: ${objectPath}`);
2367
+ return targetObject;
2368
+ }
2369
+
2370
+ // Verificar si ya está siendo trackeado
2371
+ if (this.trackedObjects.has(objectPath)) {
2372
+ console.warn(`SyntropyFront: Objeto ya está siendo trackeado: ${objectPath}`);
2373
+ return this.trackedObjects.get(objectPath).proxy;
2374
+ }
2375
+
2376
+ try {
2377
+ // Crear estado inicial
2378
+ const initialState = {
2379
+ value: this.deepClone(targetObject),
2380
+ timestamp: new Date().toISOString(),
2381
+ changeType: 'initial'
2382
+ };
2383
+
2384
+ // Crear info del objeto trackeado
2385
+ const proxyInfo = {
2386
+ objectPath,
2387
+ originalObject: targetObject,
2388
+ states: [initialState],
2389
+ proxy: null,
2390
+ options: {
2391
+ trackNested: options.trackNested !== false,
2392
+ trackArrays: options.trackArrays !== false,
2393
+ trackFunctions: options.trackFunctions !== false,
2394
+ maxDepth: options.maxDepth || 5
2395
+ }
2396
+ };
2397
+
2398
+ // Crear Proxy
2399
+ const proxy = this.createProxy(targetObject, proxyInfo);
2400
+ proxyInfo.proxy = proxy;
2401
+
2402
+ // Guardar en el mapa
2403
+ this.trackedObjects.set(objectPath, proxyInfo);
2404
+
2405
+ console.log(`SyntropyFront: Objeto agregado para tracking reactivo: ${objectPath}`);
2406
+ return proxy;
2407
+
2408
+ } catch (error) {
2409
+ console.error(`SyntropyFront: Error creando proxy para ${objectPath}:`, error);
2410
+ return targetObject;
2411
+ }
2412
+ }
2413
+
2414
+ /**
2415
+ * Crea un Proxy que intercepta cambios
2416
+ * @param {Object} target - Objeto objetivo
2417
+ * @param {Object} proxyInfo - Información del proxy
2418
+ * @param {number} depth - Profundidad actual
2419
+ * @returns {Proxy} Proxy del objeto
2420
+ */
2421
+ createProxy(target, proxyInfo, depth = 0) {
2422
+ const { objectPath, options } = proxyInfo;
2423
+
2424
+ return new Proxy(target, {
2425
+ get: (obj, prop) => {
2426
+ const value = obj[prop];
2427
+
2428
+ // Si es un objeto/array y queremos trackear anidados
2429
+ if (options.trackNested &&
2430
+ depth < options.maxDepth &&
2431
+ value &&
2432
+ typeof value === 'object' &&
2433
+ !(value instanceof Date) &&
2434
+ !(value instanceof RegExp) &&
2435
+ !(value instanceof Error)) {
2436
+
2437
+ // Crear proxy para objetos anidados
2438
+ if (Array.isArray(value) && options.trackArrays) {
2439
+ return this.createArrayProxy(value, proxyInfo, depth + 1);
2440
+ } else if (!Array.isArray(value)) {
2441
+ return this.createProxy(value, proxyInfo, depth + 1);
2442
+ }
2443
+ }
2444
+
2445
+ return value;
2446
+ },
2447
+
2448
+ set: (obj, prop, value) => {
2449
+ const oldValue = obj[prop];
2450
+
2451
+ // Solo registrar si realmente cambió
2452
+ if (!this.isEqual(oldValue, value)) {
2453
+ // Guardar estado anterior antes del cambio
2454
+ this.saveState(proxyInfo, 'property_change', {
2455
+ property: prop,
2456
+ oldValue: this.deepClone(oldValue),
2457
+ newValue: this.deepClone(value),
2458
+ path: `${objectPath}.${prop}`
2459
+ });
2460
+
2461
+ // Aplicar el cambio
2462
+ obj[prop] = value;
2463
+
2464
+ // Notificar cambio
2465
+ this.notifyChange(proxyInfo, prop, oldValue, value);
2466
+ }
2467
+
2468
+ return true;
2469
+ },
2470
+
2471
+ deleteProperty: (obj, prop) => {
2472
+ const oldValue = obj[prop];
2473
+
2474
+ // Guardar estado antes de eliminar
2475
+ this.saveState(proxyInfo, 'property_deleted', {
2476
+ property: prop,
2477
+ oldValue: this.deepClone(oldValue),
2478
+ path: `${objectPath}.${prop}`
2479
+ });
2480
+
2481
+ // Eliminar la propiedad
2482
+ const result = delete obj[prop];
2483
+
2484
+ // Notificar cambio
2485
+ this.notifyChange(proxyInfo, prop, oldValue, undefined);
2486
+
2487
+ return result;
2488
+ }
2489
+ });
2490
+ }
2491
+
2492
+ /**
2493
+ * Crea un Proxy especial para arrays
2494
+ * @param {Array} target - Array objetivo
2495
+ * @param {Object} proxyInfo - Información del proxy
2496
+ * @param {number} depth - Profundidad actual
2497
+ * @returns {Proxy} Proxy del array
2498
+ */
2499
+ createArrayProxy(target, proxyInfo, depth = 0) {
2500
+ const { objectPath, options } = proxyInfo;
2501
+
2502
+ return new Proxy(target, {
2503
+ get: (obj, prop) => {
2504
+ const value = obj[prop];
2505
+
2506
+ // Si es un método de array que modifica
2507
+ if (typeof value === 'function' && this.isArrayMutator(prop)) {
2508
+ return (...args) => {
2509
+ // Guardar estado antes de la mutación
2510
+ this.saveState(proxyInfo, 'array_mutation', {
2511
+ method: prop,
2512
+ arguments: args,
2513
+ oldArray: this.deepClone(obj),
2514
+ path: objectPath
2515
+ });
2516
+
2517
+ // Ejecutar el método
2518
+ const result = value.apply(obj, args);
2519
+
2520
+ // Notificar cambio
2521
+ this.notifyChange(proxyInfo, prop, null, obj);
2522
+
2523
+ return result;
2524
+ };
2525
+ }
2526
+
2527
+ // Si es un elemento del array y es un objeto
2528
+ if (options.trackNested &&
2529
+ depth < options.maxDepth &&
2530
+ value &&
2531
+ typeof value === 'object' &&
2532
+ !Array.isArray(value) &&
2533
+ !(value instanceof Date) &&
2534
+ !(value instanceof RegExp) &&
2535
+ !(value instanceof Error)) {
2536
+
2537
+ return this.createProxy(value, proxyInfo, depth + 1);
2538
+ }
2539
+
2540
+ return value;
2541
+ },
2542
+
2543
+ set: (obj, prop, value) => {
2544
+ const oldValue = obj[prop];
2545
+
2546
+ // Solo registrar si realmente cambió
2547
+ if (!this.isEqual(oldValue, value)) {
2548
+ // Guardar estado anterior
2549
+ this.saveState(proxyInfo, 'array_element_change', {
2550
+ index: prop,
2551
+ oldValue: this.deepClone(oldValue),
2552
+ newValue: this.deepClone(value),
2553
+ path: `${objectPath}[${prop}]`
2554
+ });
2555
+
2556
+ // Aplicar el cambio
2557
+ obj[prop] = value;
2558
+
2559
+ // Notificar cambio
2560
+ this.notifyChange(proxyInfo, prop, oldValue, value);
2561
+ }
2562
+
2563
+ return true;
2564
+ }
2565
+ });
2566
+ }
2567
+
2568
+ /**
2569
+ * Verifica si un método de array es mutador
2570
+ * @param {string} method - Nombre del método
2571
+ * @returns {boolean} True si es mutador
2572
+ */
2573
+ isArrayMutator(method) {
2574
+ const mutators = [
2575
+ 'push', 'pop', 'shift', 'unshift', 'splice',
2576
+ 'reverse', 'sort', 'fill', 'copyWithin'
2577
+ ];
2578
+ return mutators.includes(method);
2579
+ }
2580
+
2581
+ /**
2582
+ * Guarda un estado en el historial
2583
+ * @param {Object} proxyInfo - Información del proxy
2584
+ * @param {string} changeType - Tipo de cambio
2585
+ * @param {Object} changeData - Datos del cambio
2586
+ */
2587
+ saveState(proxyInfo, changeType, changeData = {}) {
2588
+ const state = {
2589
+ value: this.deepClone(proxyInfo.originalObject),
2590
+ timestamp: new Date().toISOString(),
2591
+ changeType,
2592
+ changeData
2593
+ };
2594
+
2595
+ // Agregar al historial
2596
+ proxyInfo.states.push(state);
2597
+
2598
+ // Mantener solo los últimos maxStates
2599
+ if (proxyInfo.states.length > this.maxStates) {
2600
+ proxyInfo.states.shift();
2601
+ }
2602
+ }
2603
+
2604
+ /**
2605
+ * Notifica un cambio
2606
+ * @param {Object} proxyInfo - Información del proxy
2607
+ * @param {string} property - Propiedad que cambió
2608
+ * @param {any} oldValue - Valor anterior
2609
+ * @param {any} newValue - Valor nuevo
2610
+ */
2611
+ notifyChange(proxyInfo, property, oldValue, newValue) {
2612
+ if (this.onChangeCallback) {
2613
+ try {
2614
+ this.onChangeCallback({
2615
+ objectPath: proxyInfo.objectPath,
2616
+ property,
2617
+ oldValue,
2618
+ newValue,
2619
+ timestamp: new Date().toISOString(),
2620
+ states: proxyInfo.states.length
2621
+ });
2622
+ } catch (error) {
2623
+ console.error('SyntropyFront: Error en callback de cambio:', error);
2624
+ }
2625
+ }
2626
+ }
2627
+
2628
+ /**
2629
+ * Obtiene el historial de estados de un objeto
2630
+ * @param {string} objectPath - Ruta del objeto
2631
+ * @returns {Array} Historial de estados
2632
+ */
2633
+ getObjectHistory(objectPath) {
2634
+ const proxyInfo = this.trackedObjects.get(objectPath);
2635
+ if (!proxyInfo) {
2636
+ console.warn(`SyntropyFront: Objeto no encontrado: ${objectPath}`);
2637
+ return [];
2638
+ }
2639
+
2640
+ return [...proxyInfo.states];
2641
+ }
2642
+
2643
+ /**
2644
+ * Obtiene el estado actual de un objeto
2645
+ * @param {string} objectPath - Ruta del objeto
2646
+ * @returns {Object|null} Estado actual
2647
+ */
2648
+ getCurrentState(objectPath) {
2649
+ const proxyInfo = this.trackedObjects.get(objectPath);
2650
+ if (!proxyInfo) {
2651
+ return null;
2652
+ }
2653
+
2654
+ return {
2655
+ value: this.deepClone(proxyInfo.originalObject),
2656
+ timestamp: new Date().toISOString(),
2657
+ statesCount: proxyInfo.states.length
2658
+ };
2659
+ }
2660
+
2661
+ /**
2662
+ * Obtiene todos los objetos trackeados
2663
+ * @returns {Array} Lista de objetos trackeados
2664
+ */
2665
+ getTrackedObjects() {
2666
+ return Array.from(this.trackedObjects.keys());
2667
+ }
2668
+
2669
+ /**
2670
+ * Remueve un objeto del tracking
2671
+ * @param {string} objectPath - Ruta del objeto
2672
+ * @returns {Object|null} Objeto original (sin proxy)
2673
+ */
2674
+ removeObject(objectPath) {
2675
+ const proxyInfo = this.trackedObjects.get(objectPath);
2676
+ if (!proxyInfo) {
2677
+ return null;
2678
+ }
2679
+
2680
+ this.trackedObjects.delete(objectPath);
2681
+ console.log(`SyntropyFront: Objeto removido del tracking: ${objectPath}`);
2682
+
2683
+ return proxyInfo.originalObject;
2684
+ }
2685
+
2686
+ /**
2687
+ * Limpia todos los objetos trackeados
2688
+ */
2689
+ clear() {
2690
+ this.trackedObjects.clear();
2691
+ console.log('SyntropyFront: Todos los objetos removidos del tracking');
2692
+ }
2693
+
2694
+ /**
2695
+ * Obtiene estadísticas del tracker
2696
+ * @returns {Object} Estadísticas
2697
+ */
2698
+ getStats() {
2699
+ const stats = {
2700
+ trackedObjects: this.trackedObjects.size,
2701
+ totalStates: 0,
2702
+ isEnabled: this.isEnabled,
2703
+ maxStates: this.maxStates
2704
+ };
2705
+
2706
+ for (const proxyInfo of this.trackedObjects.values()) {
2707
+ stats.totalStates += proxyInfo.states.length;
2708
+ }
2709
+
2710
+ return stats;
2711
+ }
2712
+
2713
+ /**
2714
+ * Clona profundamente un objeto
2715
+ * @param {any} obj - Objeto a clonar
2716
+ * @returns {any} Objeto clonado
2717
+ */
2718
+ deepClone(obj) {
2719
+ if (obj === null || obj === undefined) {
2720
+ return obj;
2721
+ }
2722
+
2723
+ if (typeof obj !== 'object') {
2724
+ return obj;
2725
+ }
2726
+
2727
+ if (obj instanceof Date) {
2728
+ return new Date(obj.getTime());
2729
+ }
2730
+
2731
+ if (obj instanceof RegExp) {
2732
+ return new RegExp(obj.source, obj.flags);
2733
+ }
2734
+
2735
+ if (obj instanceof Error) {
2736
+ const error = new Error(obj.message);
2737
+ error.name = obj.name;
2738
+ error.stack = obj.stack;
2739
+ if (obj.cause) {
2740
+ error.cause = this.deepClone(obj.cause);
2741
+ }
2742
+ return error;
2743
+ }
2744
+
2745
+ if (Array.isArray(obj)) {
2746
+ return obj.map(item => this.deepClone(item));
2747
+ }
2748
+
2749
+ const cloned = {};
2750
+ for (const key in obj) {
2751
+ if (obj.hasOwnProperty(key)) {
2752
+ cloned[key] = this.deepClone(obj[key]);
2753
+ }
2754
+ }
2755
+
2756
+ return cloned;
2757
+ }
2758
+
2759
+ /**
2760
+ * Compara dos valores para verificar si son iguales
2761
+ * @param {any} a - Primer valor
2762
+ * @param {any} b - Segundo valor
2763
+ * @returns {boolean} True si son iguales
2764
+ */
2765
+ isEqual(a, b) {
2766
+ if (a === b) return true;
2767
+ if (a === null || b === null) return a === b;
2768
+ if (typeof a !== typeof b) return false;
2769
+ if (typeof a !== 'object') return a === b;
2770
+
2771
+ // Para objetos, comparación superficial
2772
+ const keysA = Object.keys(a);
2773
+ const keysB = Object.keys(b);
2774
+
2775
+ if (keysA.length !== keysB.length) return false;
2776
+
2777
+ for (const key of keysA) {
2778
+ if (!keysB.includes(key)) return false;
2779
+ if (a[key] !== b[key]) return false;
2780
+ }
2781
+
2782
+ return true;
2783
+ }
2784
+ }
2785
+
2786
+ // Instancia singleton
2787
+ const proxyObjectTracker = new ProxyObjectTracker();
2788
+
2789
+ var ProxyObjectTracker$1 = /*#__PURE__*/Object.freeze({
2790
+ __proto__: null,
2791
+ ProxyObjectTracker: ProxyObjectTracker,
2792
+ proxyObjectTracker: proxyObjectTracker
2793
+ });
2794
+
2795
+ /**
2796
+ * InterceptorRegistry - Registro de interceptores personalizados
2797
+ * Permite al usuario inyectar sus propios interceptores sin modificar el código base
2798
+ * Usa una Facade para exponer solo métodos seguros a los interceptores
2799
+ */
2800
+ class InterceptorRegistry {
2801
+ constructor() {
2802
+ this.customInterceptors = new Map();
2803
+ this.isInitialized = false;
2804
+ }
2805
+
2806
+ /**
2807
+ * Crea una API segura para los interceptores
2808
+ * Solo expone métodos públicos y seguros
2809
+ * @param {Object} config - Configuración con instancias internas
2810
+ * @returns {Object} API segura para interceptores
2811
+ */
2812
+ createInterceptorApi(config) {
2813
+ const { breadcrumbStore, agent, contextCollector } = config;
2814
+
2815
+ return {
2816
+ // Métodos para breadcrumbs
2817
+ addBreadcrumb: (category, message, data = {}) => {
2818
+ breadcrumbStore.add({ category, message, data, timestamp: new Date().toISOString() });
2819
+ },
2820
+
2821
+ // Métodos para enviar datos
2822
+ sendError: (errorPayload, context = null) => {
2823
+ agent.sendError(errorPayload, context);
2824
+ },
2825
+
2826
+ sendBreadcrumbs: (breadcrumbs) => {
2827
+ agent.sendBreadcrumbs(breadcrumbs);
2828
+ },
2829
+
2830
+ // Métodos para contexto
2831
+ getContext: (contextConfig = {}) => {
2832
+ return contextCollector.collect(contextConfig);
2833
+ },
2834
+
2835
+ // Métodos de utilidad
2836
+ getTimestamp: () => new Date().toISOString(),
2837
+
2838
+ // Información de la API (solo lectura)
2839
+ apiVersion: '1.0.0',
2840
+ availableMethods: [
2841
+ 'addBreadcrumb',
2842
+ 'sendError',
2843
+ 'sendBreadcrumbs',
2844
+ 'getContext',
2845
+ 'getTimestamp'
2846
+ ]
2847
+ };
2848
+ }
2849
+
2850
+ /**
2851
+ * Registra un interceptor personalizado
2852
+ * @param {string} name - Nombre del interceptor
2853
+ * @param {Object} interceptor - Objeto interceptor con métodos init/destroy
2854
+ */
2855
+ register(name, interceptor) {
2856
+ if (!interceptor || typeof interceptor.init !== 'function') {
2857
+ throw new Error(`Interceptor ${name} debe tener un método init()`);
2858
+ }
2859
+
2860
+ this.customInterceptors.set(name, {
2861
+ name,
2862
+ interceptor,
2863
+ enabled: true
2864
+ });
2865
+
2866
+ console.log(`SyntropyFront: Interceptor personalizado registrado: ${name}`);
2867
+ }
2868
+
2869
+ /**
2870
+ * Remueve un interceptor personalizado
2871
+ * @param {string} name - Nombre del interceptor
2872
+ */
2873
+ unregister(name) {
2874
+ const registered = this.customInterceptors.get(name);
2875
+ if (registered) {
2876
+ // Destruir el interceptor si está inicializado
2877
+ if (this.isInitialized && registered.interceptor.destroy) {
2878
+ try {
2879
+ registered.interceptor.destroy();
2880
+ } catch (error) {
2881
+ console.warn(`SyntropyFront: Error destruyendo interceptor ${name}:`, error);
2882
+ }
2883
+ }
2884
+
2885
+ this.customInterceptors.delete(name);
2886
+ console.log(`SyntropyFront: Interceptor personalizado removido: ${name}`);
2887
+ }
2888
+ }
2889
+
2890
+ /**
2891
+ * Inicializa todos los interceptores personalizados
2892
+ * @param {Object} config - Configuración con instancias internas
2893
+ */
2894
+ init(config = {}) {
2895
+ if (this.isInitialized) {
2896
+ console.warn('SyntropyFront: InterceptorRegistry ya está inicializado');
2897
+ return;
2898
+ }
2899
+
2900
+ // Crear API segura para interceptores
2901
+ const interceptorApi = this.createInterceptorApi(config);
2902
+
2903
+ for (const [name, registered] of this.customInterceptors) {
2904
+ if (registered.enabled) {
2905
+ try {
2906
+ // ✅ SEGURO: Pasar solo la API, no el config crudo
2907
+ registered.interceptor.init(interceptorApi);
2908
+ console.log(`SyntropyFront: Interceptor ${name} inicializado`);
2909
+ } catch (error) {
2910
+ console.error(`SyntropyFront: Error inicializando interceptor ${name}:`, error);
2911
+ }
2912
+ }
2913
+ }
2914
+
2915
+ this.isInitialized = true;
2916
+ }
2917
+
2918
+ /**
2919
+ * Destruye todos los interceptores personalizados
2920
+ */
2921
+ destroy() {
2922
+ if (!this.isInitialized) return;
2923
+
2924
+ for (const [name, registered] of this.customInterceptors) {
2925
+ if (registered.interceptor.destroy) {
2926
+ try {
2927
+ registered.interceptor.destroy();
2928
+ console.log(`SyntropyFront: Interceptor ${name} destruido`);
2929
+ } catch (error) {
2930
+ console.warn(`SyntropyFront: Error destruyendo interceptor ${name}:`, error);
2931
+ }
2932
+ }
2933
+ }
2934
+
2935
+ this.isInitialized = false;
2936
+ }
2937
+
2938
+ /**
2939
+ * Habilita/deshabilita un interceptor personalizado
2940
+ * @param {string} name - Nombre del interceptor
2941
+ * @param {boolean} enabled - Si está habilitado
2942
+ */
2943
+ setEnabled(name, enabled) {
2944
+ const registered = this.customInterceptors.get(name);
2945
+ if (registered) {
2946
+ registered.enabled = enabled;
2947
+
2948
+ if (this.isInitialized) {
2949
+ if (enabled && registered.interceptor.init) {
2950
+ try {
2951
+ // Crear API segura para el interceptor
2952
+ const interceptorApi = this.createInterceptorApi({
2953
+ breadcrumbStore: this.breadcrumbStore,
2954
+ agent: this.agent,
2955
+ contextCollector: this.contextCollector
2956
+ });
2957
+ registered.interceptor.init(interceptorApi);
2958
+ console.log(`SyntropyFront: Interceptor ${name} habilitado`);
2959
+ } catch (error) {
2960
+ console.error(`SyntropyFront: Error habilitando interceptor ${name}:`, error);
2961
+ }
2962
+ } else if (!enabled && registered.interceptor.destroy) {
2963
+ try {
2964
+ registered.interceptor.destroy();
2965
+ console.log(`SyntropyFront: Interceptor ${name} deshabilitado`);
2966
+ } catch (error) {
2967
+ console.warn(`SyntropyFront: Error deshabilitando interceptor ${name}:`, error);
2968
+ }
2969
+ }
2970
+ }
2971
+ }
2972
+ }
2973
+
2974
+ /**
2975
+ * Obtiene la lista de interceptores registrados
2976
+ * @returns {Array} Lista de nombres de interceptores
2977
+ */
2978
+ getRegisteredInterceptors() {
2979
+ return Array.from(this.customInterceptors.keys());
2980
+ }
2981
+
2982
+ /**
2983
+ * Obtiene información de un interceptor específico
2984
+ * @param {string} name - Nombre del interceptor
2985
+ * @returns {Object|null} Información del interceptor
2986
+ */
2987
+ getInterceptorInfo(name) {
2988
+ const registered = this.customInterceptors.get(name);
2989
+ if (registered) {
2990
+ return {
2991
+ name: registered.name,
2992
+ enabled: registered.enabled,
2993
+ hasInit: typeof registered.interceptor.init === 'function',
2994
+ hasDestroy: typeof registered.interceptor.destroy === 'function'
2995
+ };
2996
+ }
2997
+ return null;
2998
+ }
2999
+
3000
+ /**
3001
+ * Obtiene la documentación de la API para interceptores
3002
+ * @returns {Object} Documentación de la API
3003
+ */
3004
+ getApiDocumentation() {
3005
+ return {
3006
+ version: '1.0.0',
3007
+ methods: {
3008
+ addBreadcrumb: {
3009
+ description: 'Agrega un breadcrumb al historial',
3010
+ signature: 'addBreadcrumb(category, message, data?)',
3011
+ example: 'api.addBreadcrumb("ui", "Usuario hizo click", { element: "button" })'
3012
+ },
3013
+ sendError: {
3014
+ description: 'Envía un error al backend',
3015
+ signature: 'sendError(errorPayload, context?)',
3016
+ example: 'api.sendError({ message: "Error crítico" }, { device: true })'
3017
+ },
3018
+ sendBreadcrumbs: {
3019
+ description: 'Envía breadcrumbs al backend',
3020
+ signature: 'sendBreadcrumbs(breadcrumbs)',
3021
+ example: 'api.sendBreadcrumbs([{ category: "ui", message: "Click" }])'
3022
+ },
3023
+ getContext: {
3024
+ description: 'Obtiene contexto del navegador',
3025
+ signature: 'getContext(contextConfig?)',
3026
+ example: 'api.getContext({ device: true, window: ["url"] })'
3027
+ },
3028
+ getTimestamp: {
3029
+ description: 'Obtiene timestamp actual en formato ISO',
3030
+ signature: 'getTimestamp()',
3031
+ example: 'const now = api.getTimestamp()'
3032
+ }
3033
+ }
3034
+ };
3035
+ }
3036
+
3037
+ /**
3038
+ * Limpia todos los interceptores registrados
3039
+ */
3040
+ clear() {
3041
+ this.destroy();
3042
+ this.customInterceptors.clear();
3043
+ }
3044
+ }
3045
+
3046
+ // Instancia singleton
3047
+ const interceptorRegistry = new InterceptorRegistry();
3048
+
3049
+ var InterceptorRegistry$1 = /*#__PURE__*/Object.freeze({
3050
+ __proto__: null,
3051
+ InterceptorRegistry: InterceptorRegistry,
3052
+ interceptorRegistry: interceptorRegistry
3053
+ });
3054
+
3055
+ /**
3056
+ * WorkerManager - Maneja comunicación con SyntropyWorker
3057
+ * Proporciona API para interactuar con el worker desde el main thread
3058
+ *
3059
+ * @author SyntropyFront Team
3060
+ * @version 1.0.0
3061
+ */
3062
+
3063
+ class WorkerManager {
3064
+ constructor() {
3065
+ this.worker = null;
3066
+ this.pendingRequests = new Map();
3067
+ this.requestId = 0;
3068
+ this.isInitialized = false;
3069
+ this.config = {};
3070
+
3071
+ // Setup worker communication
3072
+ this.setupWorker();
3073
+ }
3074
+
3075
+ /**
3076
+ * Inicializa el worker
3077
+ */
3078
+ setupWorker() {
3079
+ try {
3080
+ // Crear worker
3081
+ this.worker = new Worker('./src/workers/SyntropyWorker.js');
3082
+
3083
+ // Setup message handling
3084
+ this.worker.addEventListener('message', (event) => {
3085
+ this.handleWorkerMessage(event.data);
3086
+ });
3087
+
3088
+ // Setup error handling
3089
+ this.worker.addEventListener('error', (error) => {
3090
+ console.error('SyntropyWorker error:', error);
3091
+ this.handleWorkerError(error);
3092
+ });
3093
+
3094
+ console.log('🔄 WorkerManager: Worker inicializado');
3095
+ } catch (error) {
3096
+ console.error('WorkerManager: Error inicializando worker:', error);
3097
+ this.handleWorkerUnavailable();
3098
+ }
3099
+ }
3100
+
3101
+ /**
3102
+ * Inicializa el worker con configuración
3103
+ */
3104
+ async init(config) {
3105
+ try {
3106
+ this.config = config;
3107
+
3108
+ const response = await this.sendMessage('INIT', config);
3109
+
3110
+ if (response.success) {
3111
+ this.isInitialized = true;
3112
+ console.log('✅ WorkerManager: Worker inicializado correctamente');
3113
+ return true;
3114
+ } else {
3115
+ throw new Error(response.error || 'Error inicializando worker');
3116
+ }
3117
+ } catch (error) {
3118
+ console.error('WorkerManager: Error en init:', error);
3119
+ return false;
3120
+ }
3121
+ }
3122
+
3123
+ /**
3124
+ * Envía mensaje al worker y espera respuesta
3125
+ */
3126
+ sendMessage(type, payload = {}) {
3127
+ return new Promise((resolve, reject) => {
3128
+ if (!this.worker) {
3129
+ reject(new Error('Worker no disponible'));
3130
+ return;
3131
+ }
3132
+
3133
+ const id = this.generateRequestId();
3134
+
3135
+ // Guardar callback para la respuesta
3136
+ this.pendingRequests.set(id, { resolve, reject });
3137
+
3138
+ // Enviar mensaje al worker
3139
+ this.worker.postMessage({
3140
+ type,
3141
+ payload,
3142
+ id
3143
+ });
3144
+
3145
+ // Timeout para evitar requests colgados
3146
+ setTimeout(() => {
3147
+ if (this.pendingRequests.has(id)) {
3148
+ this.pendingRequests.delete(id);
3149
+ reject(new Error(`Timeout en request: ${type}`));
3150
+ }
3151
+ }, 5000); // 5 segundos timeout
3152
+ });
3153
+ }
3154
+
3155
+ /**
3156
+ * Maneja mensajes del worker
3157
+ */
3158
+ handleWorkerMessage(data) {
3159
+ const { id, success, error, ...response } = data;
3160
+
3161
+ const request = this.pendingRequests.get(id);
3162
+ if (request) {
3163
+ this.pendingRequests.delete(id);
3164
+
3165
+ if (success) {
3166
+ request.resolve(response);
3167
+ } else {
3168
+ request.reject(new Error(error || 'Error en worker'));
3169
+ }
3170
+ } else {
3171
+ console.warn('WorkerManager: Respuesta sin request pendiente:', id);
3172
+ }
3173
+ }
3174
+
3175
+ /**
3176
+ * Maneja errores del worker
3177
+ */
3178
+ handleWorkerError(error) {
3179
+ console.error('WorkerManager: Error del worker:', error);
3180
+
3181
+ // Limpiar requests pendientes
3182
+ this.pendingRequests.forEach((request) => {
3183
+ request.reject(new Error('Worker error'));
3184
+ });
3185
+ this.pendingRequests.clear();
3186
+
3187
+ // Fallback a modo sin worker
3188
+ this.handleWorkerUnavailable();
3189
+ }
3190
+
3191
+ /**
3192
+ * Maneja cuando el worker no está disponible
3193
+ */
3194
+ handleWorkerUnavailable() {
3195
+ console.warn('WorkerManager: Worker no disponible, usando fallback');
3196
+
3197
+ // Aquí podríamos implementar fallback al modo main thread
3198
+ // Por ahora solo loggeamos
3199
+ }
3200
+
3201
+ /**
3202
+ * Agrega breadcrumb al worker
3203
+ */
3204
+ async addBreadcrumb(type, message, data = {}) {
3205
+ try {
3206
+ const response = await this.sendMessage('ADD_BREADCRUMB', {
3207
+ type,
3208
+ message,
3209
+ data
3210
+ });
3211
+
3212
+ return response;
3213
+ } catch (error) {
3214
+ console.error('WorkerManager: Error agregando breadcrumb:', error);
3215
+ throw error;
3216
+ }
3217
+ }
3218
+
3219
+ /**
3220
+ * Obtiene breadcrumbs del worker
3221
+ */
3222
+ async getBreadcrumbs() {
3223
+ try {
3224
+ const response = await this.sendMessage('GET_BREADCRUMBS');
3225
+ return response.breadcrumbs || [];
3226
+ } catch (error) {
3227
+ console.error('WorkerManager: Error obteniendo breadcrumbs:', error);
3228
+ return [];
3229
+ }
3230
+ }
3231
+
3232
+ /**
3233
+ * Limpia breadcrumbs del worker
3234
+ */
3235
+ async clearBreadcrumbs() {
3236
+ try {
3237
+ const response = await this.sendMessage('CLEAR_BREADCRUMBS');
3238
+ return response;
3239
+ } catch (error) {
3240
+ console.error('WorkerManager: Error limpiando breadcrumbs:', error);
3241
+ throw error;
3242
+ }
3243
+ }
3244
+
3245
+ /**
3246
+ * Envía error al worker
3247
+ */
3248
+ async sendError(error, context = {}) {
3249
+ try {
3250
+ const response = await this.sendMessage('SEND_ERROR', {
3251
+ error,
3252
+ context
3253
+ });
3254
+
3255
+ return response;
3256
+ } catch (error) {
3257
+ console.error('WorkerManager: Error enviando error:', error);
3258
+ throw error;
3259
+ }
3260
+ }
3261
+
3262
+ /**
3263
+ * Actualiza contexto del worker
3264
+ */
3265
+ async updateContext(context) {
3266
+ try {
3267
+ const response = await this.sendMessage('UPDATE_CONTEXT', context);
3268
+ return response;
3269
+ } catch (error) {
3270
+ console.error('WorkerManager: Error actualizando contexto:', error);
3271
+ throw error;
3272
+ }
3273
+ }
3274
+
3275
+ /**
3276
+ * Ping al worker para verificar conectividad
3277
+ */
3278
+ async ping() {
3279
+ try {
3280
+ const response = await this.sendMessage('PING');
3281
+ return response;
3282
+ } catch (error) {
3283
+ console.error('WorkerManager: Error en ping:', error);
3284
+ throw error;
3285
+ }
3286
+ }
3287
+
3288
+ /**
3289
+ * Obtiene estadísticas del worker
3290
+ */
3291
+ async getWorkerStats() {
3292
+ try {
3293
+ const response = await this.sendMessage('GET_STATS');
3294
+ return response;
3295
+ } catch (error) {
3296
+ console.error('WorkerManager: Error obteniendo stats:', error);
3297
+ return null;
3298
+ }
3299
+ }
3300
+
3301
+ /**
3302
+ * Destruye el worker
3303
+ */
3304
+ destroy() {
3305
+ if (this.worker) {
3306
+ // Limpiar requests pendientes
3307
+ this.pendingRequests.forEach((request) => {
3308
+ request.reject(new Error('Worker destroyed'));
3309
+ });
3310
+ this.pendingRequests.clear();
3311
+
3312
+ // Terminar worker
3313
+ this.worker.terminate();
3314
+ this.worker = null;
3315
+
3316
+ console.log('🔄 WorkerManager: Worker destruido');
3317
+ }
3318
+ }
3319
+
3320
+ /**
3321
+ * Genera ID único para requests
3322
+ */
3323
+ generateRequestId() {
3324
+ return `req_${++this.requestId}_${Date.now()}`;
3325
+ }
3326
+
3327
+ /**
3328
+ * Verifica si el worker está disponible
3329
+ */
3330
+ isWorkerAvailable() {
3331
+ return this.worker !== null && this.isInitialized;
3332
+ }
3333
+
3334
+ /**
3335
+ * Obtiene estado del worker
3336
+ */
3337
+ getStatus() {
3338
+ return {
3339
+ isAvailable: this.isWorkerAvailable(),
3340
+ isInitialized: this.isInitialized,
3341
+ pendingRequests: this.pendingRequests.size,
3342
+ config: this.config
3343
+ };
3344
+ }
3345
+ }
3346
+
3347
+ var WorkerManager$1 = /*#__PURE__*/Object.freeze({
3348
+ __proto__: null,
3349
+ default: WorkerManager
3350
+ });
3351
+
3352
+ exports.SyntropyFront = SyntropyFront;
3353
+ exports.agent = agent;
3354
+ exports.breadcrumbStore = breadcrumbStore;
3355
+ exports.default = syntropyFront;
3356
+ exports.interceptors = interceptors;
3357
+ //# sourceMappingURL=index.cjs.map