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