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