@syntropysoft/syntropyfront 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,49 +1,43 @@
1
1
  /**
2
- * BreadcrumbStore - Almacén de huellas del usuario
3
- * Mantiene un historial de las últimas acciones del usuario
2
+ * BreadcrumbStore - User action timeline storage
3
+ * Maintains a history of the last user actions.
4
+ * Does not know about Agent or sending; the orchestrator wires onBreadcrumbAdded to decide what to do.
4
5
  */
5
6
  class BreadcrumbStore {
6
7
  constructor(maxBreadcrumbs = 25) {
7
8
  this.maxBreadcrumbs = maxBreadcrumbs;
8
9
  this.breadcrumbs = [];
9
- this.agent = null;
10
+ /** @type {((crumb: object) => void)|null} Optional callback when a breadcrumb is added (set by orchestrator). */
11
+ this.onBreadcrumbAdded = null;
10
12
  }
11
13
 
12
14
  /**
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
15
+ * Configures the maximum breadcrumb size
16
+ * @param {number} maxBreadcrumbs - New maximum size
23
17
  */
24
18
  setMaxBreadcrumbs(maxBreadcrumbs) {
25
19
  this.maxBreadcrumbs = maxBreadcrumbs;
26
-
27
- // Si el nuevo tamaño es menor, eliminar breadcrumbs excedentes
20
+
21
+ // If new size is smaller, remove excess breadcrumbs
28
22
  if (this.breadcrumbs.length > this.maxBreadcrumbs) {
29
23
  this.breadcrumbs = this.breadcrumbs.slice(-this.maxBreadcrumbs);
30
24
  }
31
25
  }
32
26
 
33
27
  /**
34
- * Obtiene el tamaño máximo actual
35
- * @returns {number} Tamaño máximo de breadcrumbs
28
+ * Gets current maximum size
29
+ * @returns {number} Maximum breadcrumb size
36
30
  */
37
31
  getMaxBreadcrumbs() {
38
32
  return this.maxBreadcrumbs;
39
33
  }
40
34
 
41
35
  /**
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
36
+ * Adds a breadcrumb to the list
37
+ * @param {Object} crumb - The breadcrumb to add
38
+ * @param {string} crumb.category - Event category (ui, network, error, etc.)
39
+ * @param {string} crumb.message - Descriptive message
40
+ * @param {Object} [crumb.data] - Optional additional data
47
41
  */
48
42
  add(crumb) {
49
43
  const breadcrumb = {
@@ -51,58 +45,57 @@ class BreadcrumbStore {
51
45
  timestamp: new Date().toISOString(),
52
46
  };
53
47
 
54
- if (this.breadcrumbs.length >= this.maxBreadcrumbs) {
55
- this.breadcrumbs.shift(); // Elimina el más antiguo
56
- }
57
-
58
- this.breadcrumbs.push(breadcrumb);
59
-
60
- // Callback opcional para logging
48
+ // Functional limit management
49
+ this.breadcrumbs = [...this.breadcrumbs, breadcrumb].slice(-this.maxBreadcrumbs);
50
+
61
51
  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
52
  try {
68
- this.agent.sendBreadcrumbs([breadcrumb]);
53
+ this.onBreadcrumbAdded(breadcrumb);
69
54
  } catch (error) {
70
- console.warn('SyntropyFront: Error enviando breadcrumb al agent:', error);
55
+ console.warn('SyntropyFront: Error in onBreadcrumbAdded:', error);
71
56
  }
72
57
  }
73
58
  }
74
59
 
75
60
  /**
76
- * Devuelve todos los breadcrumbs
77
- * @returns {Array} Copia de todos los breadcrumbs
61
+ * Returns all breadcrumbs
62
+ * @returns {Array} List of all breadcrumbs
78
63
  */
79
64
  getAll() {
80
65
  return [...this.breadcrumbs];
81
66
  }
82
67
 
83
68
  /**
84
- * Limpia todos los breadcrumbs
69
+ * Clears all breadcrumbs
85
70
  */
86
71
  clear() {
87
72
  this.breadcrumbs = [];
88
73
  }
89
74
 
90
75
  /**
91
- * Obtiene breadcrumbs por categoría
92
- * @param {string} category - Categoría a filtrar
93
- * @returns {Array} Breadcrumbs de la categoría especificada
76
+ * Gets current breadcrumb count
77
+ * @returns {number} Breadcrumb count
78
+ */
79
+ count() {
80
+ return this.breadcrumbs.length;
81
+ }
82
+
83
+ /**
84
+ * Gets breadcrumbs by category
85
+ * @param {string} category - Category to filter
86
+ * @returns {Array} Breadcrumbs of the specified category
94
87
  */
95
88
  getByCategory(category) {
96
89
  return this.breadcrumbs.filter(b => b.category === category);
97
90
  }
98
91
  }
99
92
 
100
- // Instancia singleton
93
+ // Singleton instance
101
94
  const breadcrumbStore = new BreadcrumbStore();
102
95
 
103
96
  /**
104
- * ConfigurationManager - Maneja la configuración del Agent
105
- * Responsabilidad única: Gestionar configuración y validación
97
+ * ConfigurationManager - Handles Agent configuration
98
+ * Single Responsibility: Manage configuration and validation
106
99
  */
107
100
  class ConfigurationManager {
108
101
  constructor() {
@@ -119,11 +112,12 @@ class ConfigurationManager {
119
112
  this.maxRetries = 5;
120
113
  this.baseDelay = 1000;
121
114
  this.maxDelay = 30000;
115
+ this.samplingRate = 1.0; // 100% by default
122
116
  }
123
117
 
124
118
  /**
125
- * Configura el manager
126
- * @param {Object} config - Configuración
119
+ * Configures the manager
120
+ * @param {Object} config - Configuration
127
121
  */
128
122
  configure(config) {
129
123
  this.endpoint = config.endpoint;
@@ -134,27 +128,28 @@ class ConfigurationManager {
134
128
  this.encrypt = config.encrypt || null;
135
129
  this.usePersistentBuffer = config.usePersistentBuffer === true;
136
130
  this.maxRetries = config.maxRetries || this.maxRetries;
137
-
138
- // Lógica simple: si hay batchTimeout = enviar breadcrumbs, sino = solo errores
131
+ this.samplingRate = typeof config.samplingRate === 'number' ? config.samplingRate : this.samplingRate;
132
+
133
+ // Simple logic: if batchTimeout exists = send breadcrumbs, else = errors only
139
134
  this.sendBreadcrumbs = !!config.batchTimeout;
140
135
  }
141
136
 
142
137
  /**
143
- * Verifica si el agent está habilitado
138
+ * Checks if the agent is enabled
144
139
  */
145
140
  isAgentEnabled() {
146
141
  return this.isEnabled;
147
142
  }
148
143
 
149
144
  /**
150
- * Verifica si debe enviar breadcrumbs
145
+ * Checks if it should send breadcrumbs
151
146
  */
152
147
  shouldSendBreadcrumbs() {
153
148
  return this.sendBreadcrumbs;
154
149
  }
155
150
 
156
151
  /**
157
- * Obtiene la configuración actual
152
+ * Gets the current configuration
158
153
  */
159
154
  getConfig() {
160
155
  return {
@@ -168,59 +163,60 @@ class ConfigurationManager {
168
163
  usePersistentBuffer: this.usePersistentBuffer,
169
164
  maxRetries: this.maxRetries,
170
165
  baseDelay: this.baseDelay,
171
- maxDelay: this.maxDelay
166
+ maxDelay: this.maxDelay,
167
+ samplingRate: this.samplingRate
172
168
  };
173
169
  }
174
170
  }
175
171
 
176
172
  /**
177
- * QueueManager - Maneja la cola de envío y batching
178
- * Responsabilidad única: Gestionar cola de items y batching
173
+ * QueueManager - Send queue and batching.
174
+ * Contract: add(item) enqueues or triggers flush; flush(cb) passes items to cb and clears queue; getSize/isEmpty/getAll are read-only.
179
175
  */
180
176
  class QueueManager {
177
+ /** @param {{ batchSize: number, batchTimeout: number|null }} configManager */
181
178
  constructor(configManager) {
182
179
  this.config = configManager;
183
180
  this.queue = [];
184
181
  this.batchTimer = null;
185
- this.flushCallback = null; // Callback interno para flush automático
182
+ this.flushCallback = null;
186
183
  }
187
184
 
188
185
  /**
189
- * Añade un item a la cola
190
- * @param {Object} item - Item a añadir
191
- */
186
+ * Adds an item; may trigger immediate or scheduled flush.
187
+ * @param {Object} item - Item to enqueue (non-null)
188
+ */
192
189
  add(item) {
190
+ // Guard: Avoid null items
191
+ if (!item) return;
192
+
193
193
  this.queue.push(item);
194
194
 
195
- // Enviar inmediatamente si alcanza el tamaño del batch
195
+ // Guard: Immediate flush if batchSize is reached
196
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);
197
+ return this.flush(this.flushCallback);
203
198
  }
199
+
200
+ // Guard: Only set Timer if one doesn't exist and we have a timeout configured
201
+ if (!this.config.batchTimeout || this.batchTimer) return;
202
+
203
+ this.batchTimer = setTimeout(() => {
204
+ this.flush(this.flushCallback);
205
+ }, this.config.batchTimeout);
204
206
  }
205
207
 
206
- /**
207
- * Obtiene todos los items de la cola
208
- */
208
+ /** @returns {Array<Object>} Copy of the queue */
209
209
  getAll() {
210
210
  return [...this.queue];
211
211
  }
212
212
 
213
- /**
214
- * Limpia la cola
215
- */
213
+ /** Clears the queue and cancels the timer. */
216
214
  clear() {
217
215
  this.queue = [];
218
216
  this.clearTimer();
219
217
  }
220
218
 
221
- /**
222
- * Limpia el timer
223
- */
219
+ /** Cancels the scheduled flush timer. */
224
220
  clearTimer() {
225
221
  if (this.batchTimer) {
226
222
  clearTimeout(this.batchTimer);
@@ -228,24 +224,21 @@ class QueueManager {
228
224
  }
229
225
  }
230
226
 
231
- /**
232
- * Obtiene el tamaño de la cola
233
- */
227
+ /** @returns {number} Queue length */
234
228
  getSize() {
235
229
  return this.queue.length;
236
230
  }
237
231
 
238
- /**
239
- * Verifica si la cola está vacía
240
- */
232
+ /** @returns {boolean} */
241
233
  isEmpty() {
242
234
  return this.queue.length === 0;
243
235
  }
244
236
 
245
237
  /**
246
- * Flush de la cola (método que será llamado por el Agent)
247
- * @param {Function} flushCallback - Callback para procesar los items
248
- */
238
+ * Sends current items to the callback and clears the queue.
239
+ * @param {(items: Array<Object>) => Promise<void>|void} flushCallback
240
+ * @returns {Promise<void>}
241
+ */
249
242
  async flush(flushCallback) {
250
243
  if (this.queue.length === 0) return;
251
244
 
@@ -260,10 +253,11 @@ class QueueManager {
260
253
  }
261
254
 
262
255
  /**
263
- * RetryManager - Maneja el sistema de reintentos
264
- * Responsabilidad única: Gestionar reintentos con backoff exponencial
256
+ * RetryManager - Retries with exponential backoff.
257
+ * Contract: addToRetryQueue enqueues and schedules; processRetryQueue(sendCb, removeCb) processes ready items.
265
258
  */
266
259
  class RetryManager {
260
+ /** @param {{ baseDelay: number, maxDelay: number, maxRetries: number }} configManager */
267
261
  constructor(configManager) {
268
262
  this.config = configManager;
269
263
  this.retryQueue = [];
@@ -271,14 +265,17 @@ class RetryManager {
271
265
  }
272
266
 
273
267
  /**
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
- */
268
+ * Enqueues items for retry (delay = min(baseDelay*2^(retryCount-1), maxDelay)).
269
+ * @param {Array<Object>} items
270
+ * @param {number} [retryCount=1]
271
+ * @param {number|null} [persistentId=null]
272
+ */
279
273
  addToRetryQueue(items, retryCount = 1, persistentId = null) {
274
+ // Guard: Avoid empty items
275
+ if (!items || items.length === 0) return;
276
+
280
277
  const delay = Math.min(this.config.baseDelay * Math.pow(2, retryCount - 1), this.config.maxDelay);
281
-
278
+
282
279
  this.retryQueue.push({
283
280
  items,
284
281
  retryCount,
@@ -290,71 +287,89 @@ class RetryManager {
290
287
  }
291
288
 
292
289
  /**
293
- * Programa el próximo reintento
294
- */
290
+ * Processes items with nextRetry <= now; calls sendCallback and removePersistentCallback on success.
291
+ * @param {(items: Array<Object>) => Promise<void>} sendCallback
292
+ * @param {(id: number) => Promise<void>} removePersistentCallback
293
+ * @returns {Promise<void>}
294
+ */
295
+ async processRetryQueue(sendCallback, removePersistentCallback) {
296
+ this.retryTimer = null;
297
+ if (this.retryQueue.length === 0) return;
298
+
299
+ const now = Date.now();
300
+ const itemsToRetry = this.retryQueue.filter(item => item.nextRetry <= now);
301
+
302
+ for (const item of itemsToRetry) {
303
+ await this.handleRetryItem(item, sendCallback, removePersistentCallback);
304
+ }
305
+
306
+ // Schedule next retry if items remain (Functional)
307
+ if (this.retryQueue.length > 0) {
308
+ this.scheduleRetry();
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Schedules the next retry (Guard Clause style)
314
+ */
295
315
  scheduleRetry() {
316
+ // Guard: Avoid multiple simultaneous timers
296
317
  if (this.retryTimer) return;
297
318
 
319
+ // Functional: Find the first item that needs a retry
298
320
  const nextItem = this.retryQueue.find(item => item.nextRetry <= Date.now());
299
321
  if (!nextItem) return;
300
322
 
323
+ // Guard: Calculate delay and schedule
324
+ const delay = Math.max(0, nextItem.nextRetry - Date.now());
301
325
  this.retryTimer = setTimeout(() => {
302
- this.processRetryQueue();
303
- }, Math.max(0, nextItem.nextRetry - Date.now()));
326
+ this.processRetryQueue(this.sendCallback, this.removePersistentCallback);
327
+ }, delay);
304
328
  }
305
329
 
306
330
  /**
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;
331
+ * Handles an individual retry item (SOLID: Single Responsibility)
332
+ * @private
333
+ */
334
+ async handleRetryItem(item, sendCallback, removePersistentCallback) {
335
+ try {
336
+ if (sendCallback) await sendCallback(item.items);
313
337
 
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
- }
338
+ // Success: Clear state
339
+ this.retryQueue = this.retryQueue.filter(q => q !== item);
340
+ if (item.persistentId && removePersistentCallback) {
341
+ await removePersistentCallback(item.persistentId);
347
342
  }
343
+ console.log(`SyntropyFront: Successful retry (${item.retryCount})`);
344
+ } catch (error) {
345
+ this.handleRetryFailure(item, error);
348
346
  }
347
+ }
349
348
 
350
- // Programar próximo reintento si quedan items
351
- if (this.retryQueue.length > 0) {
352
- this.scheduleRetry();
349
+ /**
350
+ * Manages a retry failure
351
+ * @private
352
+ */
353
+ handleRetryFailure(item, error) {
354
+ console.warn(`SyntropyFront: Retry ${item.retryCount} failed:`, error);
355
+
356
+ // Guard: Maximum retries reached
357
+ if (item.retryCount >= this.config.maxRetries) {
358
+ this.retryQueue = this.retryQueue.filter(q => q !== item);
359
+ console.error('SyntropyFront: Item exceeded maximum retries, data lost');
360
+ return;
353
361
  }
362
+
363
+ // Increment and reschedule
364
+ item.retryCount++;
365
+ item.nextRetry = Date.now() + Math.min(
366
+ this.config.baseDelay * Math.pow(2, item.retryCount - 1),
367
+ this.config.maxDelay
368
+ );
354
369
  }
355
370
 
356
371
  /**
357
- * Limpia la cola de reintentos
372
+ * Clears the retry queue
358
373
  */
359
374
  clear() {
360
375
  this.retryQueue = [];
@@ -362,7 +377,7 @@ class RetryManager {
362
377
  }
363
378
 
364
379
  /**
365
- * Limpia el timer
380
+ * Clears the timer
366
381
  */
367
382
  clearTimer() {
368
383
  if (this.retryTimer) {
@@ -372,14 +387,14 @@ class RetryManager {
372
387
  }
373
388
 
374
389
  /**
375
- * Obtiene el tamaño de la cola de reintentos
390
+ * Gets the retry queue size
376
391
  */
377
392
  getSize() {
378
393
  return this.retryQueue.length;
379
394
  }
380
395
 
381
396
  /**
382
- * Verifica si la cola de reintentos está vacía
397
+ * Checks if the retry queue is empty
383
398
  */
384
399
  isEmpty() {
385
400
  return this.retryQueue.length === 0;
@@ -387,8 +402,58 @@ class RetryManager {
387
402
  }
388
403
 
389
404
  /**
390
- * RobustSerializer - Serializador robusto que maneja referencias circulares
391
- * Implementa una solución similar a flatted pero sin dependencias externas
405
+ * Pure type-specific fragments: serialization/deserialization without state.
406
+ * Testable in isolation; state (circularRefs) is handled by RobustSerializer.
407
+ */
408
+
409
+ const MAX_STRING_SNIPPET_LENGTH = 200;
410
+
411
+ const serializedShapeOfDate = (date) => ({
412
+ __type: 'Date',
413
+ value: date.toISOString()
414
+ });
415
+
416
+ const serializedShapeOfError = (err, recurse) => ({
417
+ __type: 'Error',
418
+ name: err.name,
419
+ message: err.message,
420
+ stack: err.stack,
421
+ cause: err.cause ? recurse(err.cause) : undefined
422
+ });
423
+
424
+ const serializedShapeOfRegExp = (re) => ({
425
+ __type: 'RegExp',
426
+ source: re.source,
427
+ flags: re.flags
428
+ });
429
+
430
+ const serializedShapeOfFunction = (fn) => ({
431
+ __type: 'Function',
432
+ name: fn.name || 'anonymous',
433
+ length: fn.length,
434
+ toString: `${fn.toString().substring(0, MAX_STRING_SNIPPET_LENGTH)}...`
435
+ });
436
+
437
+ const serializedShapeOfUnknown = (obj) => ({
438
+ __type: 'Unknown',
439
+ constructor: obj.constructor ? obj.constructor.name : 'Unknown',
440
+ toString: `${String(obj).substring(0, MAX_STRING_SNIPPET_LENGTH)}...`
441
+ });
442
+
443
+ const restoreDate = (obj) => new Date(obj.value);
444
+ const restoreError = (obj, recurse) => {
445
+ const err = new Error(obj.message);
446
+ err.name = obj.name;
447
+ err.stack = obj.stack;
448
+ if (obj.cause) err.cause = recurse(obj.cause);
449
+ return err;
450
+ };
451
+ const restoreRegExp = (obj) => new RegExp(obj.source, obj.flags);
452
+ const restoreFunction = (obj) => `[Function: ${obj.name}]`;
453
+
454
+ /**
455
+ * RobustSerializer - Robust serialization with circular references.
456
+ * Composes pure type-specific fragments; state (seen, circularRefs) only in arrays/objects.
392
457
  */
393
458
  class RobustSerializer {
394
459
  constructor() {
@@ -397,27 +462,14 @@ class RobustSerializer {
397
462
  this.refCounter = 0;
398
463
  }
399
464
 
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
465
  serialize(obj) {
406
466
  try {
407
- // Reset state
408
467
  this.seen = new WeakSet();
409
468
  this.circularRefs = new Map();
410
469
  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);
470
+ return JSON.stringify(this.makeSerializable(obj));
417
471
  } 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
472
+ console.error('SyntropyFront: Error in robust serialization:', error);
421
473
  return JSON.stringify({
422
474
  __serializationError: true,
423
475
  error: error.message,
@@ -428,253 +480,141 @@ class RobustSerializer {
428
480
  }
429
481
  }
430
482
 
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
-
483
+ _serializeKnownType(obj, path) {
484
+ if (obj === null || obj === undefined) return obj;
485
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') return obj;
486
+ if (obj instanceof Date) return serializedShapeOfDate(obj);
455
487
  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
- };
488
+ return serializedShapeOfError(obj, (x) => this.makeSerializable(x, `${path}.cause`));
471
489
  }
490
+ if (obj instanceof RegExp) return serializedShapeOfRegExp(obj);
491
+ if (typeof obj === 'function') return serializedShapeOfFunction(obj);
492
+ return undefined;
493
+ }
472
494
 
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
- }
495
+ makeSerializable(obj, path = '') {
496
+ if (obj === null || obj === undefined) return obj;
497
+ const known = this._serializeKnownType(obj, path);
498
+ if (known !== undefined) return known;
499
+ if (Array.isArray(obj)) return this._serializeArray(obj, path);
500
+ if (typeof obj === 'object') return this._serializeObject(obj, path);
501
+ return serializedShapeOfUnknown(obj);
502
+ }
483
503
 
484
- this.seen.add(obj);
485
- const refId = `ref_${++this.refCounter}`;
486
- this.circularRefs.set(obj, refId);
504
+ _serializeArray(obj, path) {
505
+ if (this.seen.has(obj)) return { __circular: true, refId: this.circularRefs.get(obj) };
506
+ this.seen.add(obj);
507
+ this.circularRefs.set(obj, `ref_${++this.refCounter}`);
508
+ return obj.map((item, i) => this.makeSerializable(item, `${path}[${i}]`));
509
+ }
487
510
 
488
- return obj.map((item, index) =>
489
- this.makeSerializable(item, `${path}[${index}]`)
490
- );
511
+ _serializeObjectKey(result, obj, key, path) {
512
+ try {
513
+ result[key] = this.makeSerializable(obj[key], `${path}.${key}`);
514
+ } catch (error) {
515
+ result[key] = {
516
+ __serializationError: true,
517
+ error: error.message,
518
+ propertyName: key
519
+ };
491
520
  }
521
+ }
492
522
 
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
523
+ _serializeObjectSymbols(obj, path, result) {
524
+ if (!Object.getOwnPropertySymbols) return;
525
+ const symbols = Object.getOwnPropertySymbols(obj);
526
+ for (const symbol of symbols) {
527
+ const symKey = `__symbol_${symbol.description || 'anonymous'}`;
528
+ try {
529
+ result[symKey] = this.makeSerializable(obj[symbol], `${path}[Symbol]`);
530
+ } catch (error) {
531
+ result[symKey] = {
532
+ __serializationError: true,
533
+ error: error.message,
534
+ symbolName: symbol.description || 'anonymous'
501
535
  };
502
536
  }
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
537
  }
538
+ }
548
539
 
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
- };
540
+ _serializeObject(obj, path) {
541
+ if (this.seen.has(obj)) return { __circular: true, refId: this.circularRefs.get(obj) };
542
+ this.seen.add(obj);
543
+ this.circularRefs.set(obj, `ref_${++this.refCounter}`);
544
+ const result = {};
545
+ for (const key in obj) {
546
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
547
+ this._serializeObjectKey(result, obj, key, path);
557
548
  }
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
- };
549
+ this._serializeObjectSymbols(obj, path, result);
550
+ return result;
565
551
  }
566
552
 
567
553
  /**
568
- * Deserializa un objeto serializado con referencias circulares
569
- * @param {string} jsonString - JSON string a deserializar
570
- * @returns {any} Objeto deserializado
554
+ * Deserializes a serialized object with circular references
555
+ * @param {string} jsonString - JSON string to deserialize
556
+ * @returns {any} Deserialized object
571
557
  */
572
558
  deserialize(jsonString) {
573
559
  try {
574
560
  const parsed = JSON.parse(jsonString);
575
561
  return this.restoreCircularRefs(parsed);
576
562
  } catch (error) {
577
- console.error('SyntropyFront: Error en deserialización:', error);
563
+ console.error('SyntropyFront: Error in deserialization:', error);
578
564
  return null;
579
565
  }
580
566
  }
581
567
 
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
- }
568
+ _restoreKnownType(obj, refs) {
569
+ if (obj === null || obj === undefined) return obj;
570
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') return obj;
571
+ if (obj.__type === 'Date') return restoreDate(obj);
572
+ if (obj.__type === 'Error') return restoreError(obj, (x) => this.restoreCircularRefs(x, refs));
573
+ if (obj.__type === 'RegExp') return restoreRegExp(obj);
574
+ if (obj.__type === 'Function') return restoreFunction(obj);
575
+ return undefined;
576
+ }
620
577
 
621
- // Arrays
622
- if (Array.isArray(obj)) {
623
- const result = [];
624
- refs.set(obj, result);
578
+ restoreCircularRefs(obj, refs = new Map()) {
579
+ const known = this._restoreKnownType(obj, refs);
580
+ if (known !== undefined) return known;
581
+ if (Array.isArray(obj)) return this._restoreArray(obj, refs);
582
+ if (typeof obj === 'object') return this._restoreObject(obj, refs);
583
+ return obj;
584
+ }
625
585
 
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
- }
586
+ _restoreArray(obj, refs) {
587
+ const result = [];
588
+ refs.set(obj, result);
589
+ for (let i = 0; i < obj.length; i++) {
590
+ if (obj[i]?.__circular) {
591
+ result[i] = refs.has(obj[i].refId) ? refs.get(obj[i].refId) : null;
592
+ } else {
593
+ result[i] = this.restoreCircularRefs(obj[i], refs);
637
594
  }
638
-
639
- return result;
640
595
  }
596
+ return result;
597
+ }
641
598
 
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
- }
599
+ _restoreObject(obj, refs) {
600
+ const result = {};
601
+ refs.set(obj, result);
602
+ for (const key in obj) {
603
+ if (!Object.prototype.hasOwnProperty.call(obj, key) || key.startsWith('__')) continue;
604
+ const value = obj[key];
605
+ if (value?.__circular) {
606
+ result[key] = refs.has(value.refId) ? refs.get(value.refId) : null;
607
+ } else {
608
+ result[key] = this.restoreCircularRefs(value, refs);
666
609
  }
667
-
668
- return result;
669
610
  }
670
-
671
- return obj;
611
+ return result;
672
612
  }
673
613
 
674
614
  /**
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
615
+ * Safely serializes for logging (simplified version)
616
+ * @param {any} obj - Object to serialize
617
+ * @returns {string} Safe JSON string for logs
678
618
  */
679
619
  serializeForLogging(obj) {
680
620
  try {
@@ -682,7 +622,7 @@ class RobustSerializer {
682
622
  } catch (error) {
683
623
  return JSON.stringify({
684
624
  __logError: true,
685
- message: 'Error serializando para logging',
625
+ message: 'Error serializing for logging',
686
626
  originalError: error.message,
687
627
  timestamp: new Date().toISOString()
688
628
  });
@@ -690,42 +630,45 @@ class RobustSerializer {
690
630
  }
691
631
  }
692
632
 
693
- // Instancia singleton
633
+ // Singleton instance
694
634
  const robustSerializer = new RobustSerializer();
695
635
 
696
636
  /**
697
- * HttpTransport - Maneja el envío HTTP
698
- * Responsabilidad única: Gestionar envío HTTP y serialización
637
+ * HttpTransport - HTTP send to the backend.
638
+ * Contract: send(items) serializes and POSTs; applyEncryption(data) applies config.encrypt if present; isConfigured() by endpoint.
699
639
  */
700
640
  class HttpTransport {
641
+ /** @param {{ endpoint: string|null, headers: Object, encrypt?: function }} configManager */
701
642
  constructor(configManager) {
702
643
  this.config = configManager;
703
644
  }
704
645
 
705
646
  /**
706
- * Envía datos al backend
707
- * @param {Array} items - Items a enviar
708
- */
647
+ * Serializes items, applies encrypt if configured, POSTs to endpoint.
648
+ * @param {Array<Object>} items - Items to send
649
+ * @returns {Promise<Object>} response.json()
650
+ * @throws {Error} If HTTP !ok or network failure
651
+ */
709
652
  async send(items) {
710
653
  const payload = {
711
654
  timestamp: new Date().toISOString(),
712
655
  items
713
656
  };
714
657
 
715
- // ✅ SERIALIZACIÓN ROBUSTA: Usar serializador que maneja referencias circulares
658
+ // ✅ ROBUST SERIALIZATION: Use serializer that handles circular references
716
659
  let serializedPayload;
717
660
  try {
718
661
  serializedPayload = robustSerializer.serialize(payload);
719
662
  } 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
663
+ console.error('SyntropyFront: Error in payload serialization:', error);
664
+
665
+ // Fallback: attempt basic serialization with error info
723
666
  serializedPayload = JSON.stringify({
724
667
  __serializationError: true,
725
668
  error: error.message,
726
669
  timestamp: new Date().toISOString(),
727
670
  itemsCount: items.length,
728
- fallbackData: 'Serialización falló, datos no enviados'
671
+ fallbackData: 'Serialization failed, data not sent'
729
672
  });
730
673
  }
731
674
 
@@ -743,9 +686,10 @@ class HttpTransport {
743
686
  }
744
687
 
745
688
  /**
746
- * Aplica encriptación si está configurada
747
- * @param {*} data - Datos a encriptar
748
- */
689
+ * Applies encryption if configured.
690
+ * @param {*} data - Data to encrypt
691
+ * @returns {*} Encrypted data or unchanged
692
+ */
749
693
  applyEncryption(data) {
750
694
  if (this.config.encrypt) {
751
695
  return this.config.encrypt(data);
@@ -753,17 +697,15 @@ class HttpTransport {
753
697
  return data;
754
698
  }
755
699
 
756
- /**
757
- * Verifica si el transport está configurado
758
- */
700
+ /** @returns {boolean} */
759
701
  isConfigured() {
760
702
  return !!this.config.endpoint;
761
703
  }
762
704
  }
763
705
 
764
706
  /**
765
- * DatabaseConfigManager - Maneja la configuración de IndexedDB
766
- * Responsabilidad única: Validar y gestionar la configuración de la base de datos
707
+ * DatabaseConfigManager - Handles IndexedDB configuration
708
+ * Single responsibility: Validate and manage database configuration
767
709
  */
768
710
  class DatabaseConfigManager {
769
711
  constructor(dbName, dbVersion, storeName) {
@@ -773,9 +715,9 @@ class DatabaseConfigManager {
773
715
  }
774
716
 
775
717
  /**
776
- * Valida que la configuración sea correcta
777
- * @returns {Object} Resultado de validación
778
- */
718
+ * Validates that the configuration is correct
719
+ * @returns {Object} Validation result
720
+ */
779
721
  validateConfig() {
780
722
  const validationResult = {
781
723
  isValid: true,
@@ -785,26 +727,26 @@ class DatabaseConfigManager {
785
727
 
786
728
  if (!this.dbName || typeof this.dbName !== 'string') {
787
729
  validationResult.isValid = false;
788
- validationResult.errors.push('dbName debe ser un string no vacío');
730
+ validationResult.errors.push('dbName must be a non-empty string');
789
731
  }
790
732
 
791
733
  if (!this.dbVersion || typeof this.dbVersion !== 'number' || this.dbVersion < 1) {
792
734
  validationResult.isValid = false;
793
- validationResult.errors.push('dbVersion debe ser un número mayor a 0');
735
+ validationResult.errors.push('dbVersion must be a number greater than 0');
794
736
  }
795
737
 
796
738
  if (!this.storeName || typeof this.storeName !== 'string') {
797
739
  validationResult.isValid = false;
798
- validationResult.errors.push('storeName debe ser un string no vacío');
740
+ validationResult.errors.push('storeName must be a non-empty string');
799
741
  }
800
742
 
801
743
  return validationResult;
802
744
  }
803
745
 
804
746
  /**
805
- * Verifica si IndexedDB está disponible en el entorno
806
- * @returns {Object} Resultado de disponibilidad
807
- */
747
+ * Checks if IndexedDB is available in the environment
748
+ * @returns {Object} Availability result
749
+ */
808
750
  checkIndexedDBAvailability() {
809
751
  const availabilityResult = {
810
752
  isAvailable: false,
@@ -813,12 +755,12 @@ class DatabaseConfigManager {
813
755
  };
814
756
 
815
757
  if (typeof window === 'undefined') {
816
- availabilityResult.reason = 'No estamos en un entorno de browser';
758
+ availabilityResult.reason = 'Not in a browser environment';
817
759
  return availabilityResult;
818
760
  }
819
761
 
820
762
  if (!window.indexedDB) {
821
- availabilityResult.reason = 'IndexedDB no está disponible en este browser';
763
+ availabilityResult.reason = 'IndexedDB is not available in this browser';
822
764
  return availabilityResult;
823
765
  }
824
766
 
@@ -827,9 +769,9 @@ class DatabaseConfigManager {
827
769
  }
828
770
 
829
771
  /**
830
- * Obtiene la configuración actual
831
- * @returns {Object} Configuración
832
- */
772
+ * Returns the current configuration
773
+ * @returns {Object} Configuration
774
+ */
833
775
  getConfig() {
834
776
  return {
835
777
  dbName: this.dbName,
@@ -839,9 +781,9 @@ class DatabaseConfigManager {
839
781
  }
840
782
 
841
783
  /**
842
- * Crea la configuración del object store
843
- * @returns {Object} Configuración del store
844
- */
784
+ * Returns the object store configuration
785
+ * @returns {Object} Store configuration
786
+ */
845
787
  getStoreConfig() {
846
788
  return {
847
789
  keyPath: 'id',
@@ -851,8 +793,8 @@ class DatabaseConfigManager {
851
793
  }
852
794
 
853
795
  /**
854
- * DatabaseConnectionManager - Maneja la conexión con IndexedDB
855
- * Responsabilidad única: Gestionar la apertura y cierre de conexiones
796
+ * DatabaseConnectionManager - Handles IndexedDB connection.
797
+ * Single responsibility: Manage opening and closing of connections. Uses guard clauses (return early).
856
798
  */
857
799
  class DatabaseConnectionManager {
858
800
  constructor(configManager) {
@@ -862,53 +804,55 @@ class DatabaseConnectionManager {
862
804
  }
863
805
 
864
806
  /**
865
- * Inicializa la conexión con IndexedDB
866
- * @returns {Promise<Object>} Resultado de la inicialización
867
- */
807
+ * Initializes the connection to IndexedDB
808
+ * @returns {Promise<Object>} Init result
809
+ */
868
810
  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
- }
811
+ // Guard: validate configuration
812
+ const configValidation = this.configManager.validateConfig();
813
+ if (!configValidation.isValid) {
814
+ return {
815
+ success: false,
816
+ error: `Invalid configuration: ${configValidation.errors.join(', ')}`,
817
+ timestamp: new Date().toISOString()
818
+ };
819
+ }
882
820
 
883
- // Verificar disponibilidad de IndexedDB
884
- const availabilityCheck = this.configManager.checkIndexedDBAvailability();
885
- if (!availabilityCheck.isAvailable) {
886
- initResult.error = availabilityCheck.reason;
887
- return initResult;
888
- }
821
+ const availabilityCheck = this.configManager.checkIndexedDBAvailability();
822
+ if (!availabilityCheck.isAvailable) {
823
+ return {
824
+ success: false,
825
+ error: availabilityCheck.reason,
826
+ timestamp: new Date().toISOString()
827
+ };
828
+ }
889
829
 
890
- // Abrir conexión
830
+ try {
891
831
  const connectionResult = await this.openConnection();
892
832
  if (!connectionResult.success) {
893
- initResult.error = connectionResult.error;
894
- return initResult;
833
+ return {
834
+ success: false,
835
+ error: connectionResult.error,
836
+ timestamp: new Date().toISOString()
837
+ };
895
838
  }
896
839
 
897
840
  this.db = connectionResult.db;
898
841
  this.isAvailable = true;
899
- initResult.success = true;
900
842
 
901
- return initResult;
843
+ return { success: true, error: null, timestamp: new Date().toISOString() };
902
844
  } catch (error) {
903
- initResult.error = `Error inesperado: ${error.message}`;
904
- return initResult;
845
+ return {
846
+ success: false,
847
+ error: `Unexpected error: ${error.message}`,
848
+ timestamp: new Date().toISOString()
849
+ };
905
850
  }
906
851
  }
907
852
 
908
853
  /**
909
- * Abre la conexión con IndexedDB
910
- * @returns {Promise<Object>} Resultado de la conexión
911
- */
854
+ * Opens the connection to IndexedDB
855
+ */
912
856
  openConnection() {
913
857
  return new Promise((resolve) => {
914
858
  const config = this.configManager.getConfig();
@@ -917,7 +861,7 @@ class DatabaseConnectionManager {
917
861
  request.onerror = () => {
918
862
  resolve({
919
863
  success: false,
920
- error: 'Error abriendo IndexedDB',
864
+ error: 'Error opening IndexedDB',
921
865
  db: null
922
866
  });
923
867
  };
@@ -925,7 +869,7 @@ class DatabaseConnectionManager {
925
869
  request.onupgradeneeded = (event) => {
926
870
  const db = event.target.result;
927
871
  const storeConfig = this.configManager.getStoreConfig();
928
-
872
+
929
873
  if (!db.objectStoreNames.contains(config.storeName)) {
930
874
  db.createObjectStore(config.storeName, storeConfig);
931
875
  }
@@ -942,52 +886,42 @@ class DatabaseConnectionManager {
942
886
  }
943
887
 
944
888
  /**
945
- * Cierra la conexión con la base de datos
946
- * @returns {Object} Resultado del cierre
947
- */
889
+ * Closes the database connection
890
+ */
948
891
  close() {
949
- const closeResult = {
950
- success: false,
951
- error: null,
952
- timestamp: new Date().toISOString()
953
- };
892
+ // Guard: no active connection
893
+ if (!this.db) {
894
+ return { success: false, error: 'No active connection to close', timestamp: new Date().toISOString() };
895
+ }
954
896
 
955
897
  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
- }
898
+ this.db.close();
899
+ this.db = null;
900
+ this.isAvailable = false;
901
+ return { success: true, error: null, timestamp: new Date().toISOString() };
964
902
  } catch (error) {
965
- closeResult.error = `Error cerrando conexión: ${error.message}`;
903
+ return { success: false, error: `Error closing connection: ${error.message}`, timestamp: new Date().toISOString() };
966
904
  }
967
-
968
- return closeResult;
969
905
  }
970
906
 
971
907
  /**
972
- * Verifica si la base de datos está disponible
973
- * @returns {boolean} True si está disponible
974
- */
908
+ * Returns whether the database is available
909
+ */
975
910
  isDatabaseAvailable() {
976
911
  return this.isAvailable && this.db !== null;
977
912
  }
978
913
 
979
914
  /**
980
- * Obtiene la instancia de la base de datos
981
- * @returns {IDBDatabase|null} Instancia de la base de datos
982
- */
915
+ * Returns the database instance
916
+ */
983
917
  getDatabase() {
984
918
  return this.isDatabaseAvailable() ? this.db : null;
985
919
  }
986
920
  }
987
921
 
988
922
  /**
989
- * DatabaseTransactionManager - Maneja las transacciones de IndexedDB
990
- * Responsabilidad única: Gestionar transacciones de lectura y escritura
923
+ * DatabaseTransactionManager - Handles IndexedDB transactions
924
+ * Single responsibility: Manage read and write transactions
991
925
  */
992
926
  class DatabaseTransactionManager {
993
927
  constructor(connectionManager, configManager) {
@@ -996,10 +930,10 @@ class DatabaseTransactionManager {
996
930
  }
997
931
 
998
932
  /**
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
- */
933
+ * Returns a read transaction
934
+ * @returns {IDBTransaction} Read transaction
935
+ * @throws {Error} If the database is not available
936
+ */
1003
937
  getReadTransaction() {
1004
938
  this.ensureDatabaseAvailable();
1005
939
 
@@ -1010,10 +944,10 @@ class DatabaseTransactionManager {
1010
944
  }
1011
945
 
1012
946
  /**
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
- */
947
+ * Returns a write transaction
948
+ * @returns {IDBTransaction} Write transaction
949
+ * @throws {Error} If the database is not available
950
+ */
1017
951
  getWriteTransaction() {
1018
952
  this.ensureDatabaseAvailable();
1019
953
 
@@ -1024,20 +958,20 @@ class DatabaseTransactionManager {
1024
958
  }
1025
959
 
1026
960
  /**
1027
- * Obtiene el object store para una transacción
1028
- * @param {IDBTransaction} transaction - Transacción activa
1029
- * @returns {IDBObjectStore} Object store
1030
- */
961
+ * Returns the object store for a transaction
962
+ * @param {IDBTransaction} transaction - Active transaction
963
+ * @returns {IDBObjectStore} Object store
964
+ */
1031
965
  getObjectStore(transaction) {
1032
966
  const config = this.configManager.getConfig();
1033
967
  return transaction.objectStore(config.storeName);
1034
968
  }
1035
969
 
1036
970
  /**
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
- */
971
+ * Executes a read operation safely
972
+ * @param {Function} operation - Operation to execute
973
+ * @returns {Promise<Object>} Operation result
974
+ */
1041
975
  async executeReadOperation(operation) {
1042
976
  const operationResult = {
1043
977
  success: false,
@@ -1057,16 +991,16 @@ class DatabaseTransactionManager {
1057
991
 
1058
992
  return operationResult;
1059
993
  } catch (error) {
1060
- operationResult.error = `Error en operación de lectura: ${error.message}`;
994
+ operationResult.error = `Error in read operation: ${error.message}`;
1061
995
  return operationResult;
1062
996
  }
1063
997
  }
1064
998
 
1065
999
  /**
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
- */
1000
+ * Executes a write operation safely
1001
+ * @param {Function} operation - Operation to execute
1002
+ * @returns {Promise<Object>} Operation result
1003
+ */
1070
1004
  async executeWriteOperation(operation) {
1071
1005
  const operationResult = {
1072
1006
  success: false,
@@ -1086,15 +1020,15 @@ class DatabaseTransactionManager {
1086
1020
 
1087
1021
  return operationResult;
1088
1022
  } catch (error) {
1089
- operationResult.error = `Error en operación de escritura: ${error.message}`;
1023
+ operationResult.error = `Error in write operation: ${error.message}`;
1090
1024
  return operationResult;
1091
1025
  }
1092
1026
  }
1093
1027
 
1094
1028
  /**
1095
- * Verifica que la base de datos esté disponible
1096
- * @throws {Error} Si la base de datos no está disponible
1097
- */
1029
+ * Ensures the database is available
1030
+ * @throws {Error} If the database is not available
1031
+ */
1098
1032
  ensureDatabaseAvailable() {
1099
1033
  if (!this.connectionManager.isDatabaseAvailable()) {
1100
1034
  throw new Error('Database not available');
@@ -1102,9 +1036,9 @@ class DatabaseTransactionManager {
1102
1036
  }
1103
1037
 
1104
1038
  /**
1105
- * Obtiene información sobre el estado de las transacciones
1106
- * @returns {Object} Estado de las transacciones
1107
- */
1039
+ * Returns transaction status information
1040
+ * @returns {Object} Transaction status
1041
+ */
1108
1042
  getTransactionStatus() {
1109
1043
  return {
1110
1044
  isDatabaseAvailable: this.connectionManager.isDatabaseAvailable(),
@@ -1115,8 +1049,8 @@ class DatabaseTransactionManager {
1115
1049
  }
1116
1050
 
1117
1051
  /**
1118
- * DatabaseManager - Coordinador de la gestión de IndexedDB
1119
- * Responsabilidad única: Coordinar los managers especializados
1052
+ * DatabaseManager - Coordinates IndexedDB access.
1053
+ * Single responsibility: Coordinate specialized managers. Uses guard clauses.
1120
1054
  */
1121
1055
  class DatabaseManager {
1122
1056
  constructor(dbName, dbVersion, storeName) {
@@ -1126,56 +1060,57 @@ class DatabaseManager {
1126
1060
  }
1127
1061
 
1128
1062
  /**
1129
- * Inicializa la conexión con IndexedDB
1130
- */
1063
+ * Initializes the connection to IndexedDB
1064
+ */
1131
1065
  async init() {
1132
1066
  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);
1067
+
1068
+ if (!initResult.success) {
1069
+ console.warn('SyntropyFront: Error initializing database:', initResult.error);
1070
+ return false;
1138
1071
  }
1139
-
1140
- return initResult.success;
1072
+
1073
+ console.log('SyntropyFront: Database initialized');
1074
+ return true;
1141
1075
  }
1142
1076
 
1143
1077
  /**
1144
- * Obtiene una transacción de lectura
1145
- */
1078
+ * Returns a read transaction
1079
+ */
1146
1080
  getReadTransaction() {
1147
1081
  return this.transactionManager.getReadTransaction();
1148
1082
  }
1149
1083
 
1150
1084
  /**
1151
- * Obtiene una transacción de escritura
1152
- */
1085
+ * Returns a write transaction
1086
+ */
1153
1087
  getWriteTransaction() {
1154
1088
  return this.transactionManager.getWriteTransaction();
1155
1089
  }
1156
1090
 
1157
1091
  /**
1158
- * Cierra la conexión con la base de datos
1159
- */
1092
+ * Closes the database connection
1093
+ */
1160
1094
  close() {
1161
1095
  const closeResult = this.connectionManager.close();
1162
-
1096
+
1163
1097
  if (!closeResult.success) {
1164
- console.warn('SyntropyFront: Error cerrando base de datos:', closeResult.error);
1098
+ console.warn('SyntropyFront: Error closing database:', closeResult.error);
1099
+ return false;
1165
1100
  }
1166
-
1167
- return closeResult.success;
1101
+
1102
+ return true;
1168
1103
  }
1169
1104
 
1170
1105
  /**
1171
- * Verifica si la base de datos está disponible
1172
- */
1106
+ * Returns whether the database is available
1107
+ */
1173
1108
  isDatabaseAvailable() {
1174
1109
  return this.connectionManager.isDatabaseAvailable();
1175
1110
  }
1176
1111
 
1177
- // ===== Propiedades de compatibilidad =====
1178
-
1112
+ // ===== Compatibility properties =====
1113
+
1179
1114
  /**
1180
1115
  * @deprecated Usar configManager.getConfig().dbName
1181
1116
  */
@@ -1213,8 +1148,8 @@ class DatabaseManager {
1213
1148
  }
1214
1149
 
1215
1150
  /**
1216
- * StorageManager - Maneja las operaciones CRUD de IndexedDB
1217
- * Responsabilidad única: Gestionar operaciones de almacenamiento y recuperación
1151
+ * StorageManager - Handles IndexedDB CRUD operations
1152
+ * Single responsibility: Manage storage and retrieval operations
1218
1153
  */
1219
1154
  class StorageManager {
1220
1155
  constructor(databaseManager, serializationManager) {
@@ -1223,10 +1158,10 @@ class StorageManager {
1223
1158
  }
1224
1159
 
1225
1160
  /**
1226
- * Guarda items en el almacenamiento
1227
- * @param {Array} items - Items a guardar
1228
- * @returns {Promise<number>} ID del item guardado
1229
- */
1161
+ * Saves items to storage
1162
+ * @param {Array} items - Items to save
1163
+ * @returns {Promise<number>} Saved item ID
1164
+ */
1230
1165
  async save(items) {
1231
1166
  this.ensureDatabaseAvailable();
1232
1167
 
@@ -1244,9 +1179,9 @@ class StorageManager {
1244
1179
  }
1245
1180
 
1246
1181
  /**
1247
- * Obtiene todos los items del almacenamiento
1248
- * @returns {Promise<Array>} Items deserializados
1249
- */
1182
+ * Retrieves all items from storage
1183
+ * @returns {Promise<Array>} Deserialized items
1184
+ */
1250
1185
  async retrieve() {
1251
1186
  if (!this.databaseManager.isDatabaseAvailable()) {
1252
1187
  return [];
@@ -1257,10 +1192,10 @@ class StorageManager {
1257
1192
  }
1258
1193
 
1259
1194
  /**
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
- */
1195
+ * Retrieves a single item by ID
1196
+ * @param {number} id - Item ID
1197
+ * @returns {Promise<Object|null>} Deserialized item or null
1198
+ */
1264
1199
  async retrieveById(id) {
1265
1200
  if (!this.databaseManager.isDatabaseAvailable()) {
1266
1201
  return null;
@@ -1271,21 +1206,21 @@ class StorageManager {
1271
1206
  }
1272
1207
 
1273
1208
  /**
1274
- * Remueve un item del almacenamiento
1275
- * @param {number} id - ID del item a remover
1276
- * @returns {Promise<void>}
1277
- */
1209
+ * Removes an item from storage
1210
+ * @param {number} id - ID of the item to remove
1211
+ * @returns {Promise<void>}
1212
+ */
1278
1213
  async remove(id) {
1279
1214
  this.ensureDatabaseAvailable();
1280
1215
  return this.executeWriteOperation(store => store.delete(id));
1281
1216
  }
1282
1217
 
1283
1218
  /**
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
- */
1219
+ * Updates an item in storage
1220
+ * @param {number} id - Item ID
1221
+ * @param {Object} updates - Fields to update
1222
+ * @returns {Promise<number>} Updated item ID
1223
+ */
1289
1224
  async update(id, updates) {
1290
1225
  this.ensureDatabaseAvailable();
1291
1226
 
@@ -1299,20 +1234,20 @@ class StorageManager {
1299
1234
  }
1300
1235
 
1301
1236
  /**
1302
- * Limpia todo el almacenamiento
1303
- * @returns {Promise<void>}
1304
- */
1237
+ * Clears all storage
1238
+ * @returns {Promise<void>}
1239
+ */
1305
1240
  async clear() {
1306
1241
  this.ensureDatabaseAvailable();
1307
1242
  return this.executeWriteOperation(store => store.clear());
1308
1243
  }
1309
1244
 
1310
- // ===== Métodos privados declarativos =====
1245
+ // ===== Private declarative methods =====
1311
1246
 
1312
1247
  /**
1313
- * Verifica que la base de datos esté disponible
1314
- * @throws {Error} Si la base de datos no está disponible
1315
- */
1248
+ * Ensures the database is available
1249
+ * @throws {Error} If the database is not available
1250
+ */
1316
1251
  ensureDatabaseAvailable() {
1317
1252
  if (!this.databaseManager.isDatabaseAvailable()) {
1318
1253
  throw new Error('Database not available');
@@ -1320,10 +1255,10 @@ class StorageManager {
1320
1255
  }
1321
1256
 
1322
1257
  /**
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
- */
1258
+ * Executes a read operation in a declarative way
1259
+ * @param {Function} operation - Operation to run on the store
1260
+ * @returns {Promise<*>} Operation result
1261
+ */
1327
1262
  executeReadOperation(operation) {
1328
1263
  return new Promise((resolve, reject) => {
1329
1264
  try {
@@ -1340,10 +1275,10 @@ class StorageManager {
1340
1275
  }
1341
1276
 
1342
1277
  /**
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
- */
1278
+ * Executes a write operation in a declarative way
1279
+ * @param {Function} operation - Operation to run on the store
1280
+ * @returns {Promise<*>} Operation result
1281
+ */
1347
1282
  executeWriteOperation(operation) {
1348
1283
  return new Promise((resolve, reject) => {
1349
1284
  try {
@@ -1360,19 +1295,19 @@ class StorageManager {
1360
1295
  }
1361
1296
 
1362
1297
  /**
1363
- * Deserializa un array de items
1364
- * @param {Array} rawItems - Items crudos de la base de datos
1365
- * @returns {Array} Items deserializados
1366
- */
1298
+ * Deserializes an array of items
1299
+ * @param {Array} rawItems - Raw items from the database
1300
+ * @returns {Array} Deserialized items
1301
+ */
1367
1302
  deserializeItems(rawItems) {
1368
1303
  return rawItems.map(item => this.deserializeItem(item));
1369
1304
  }
1370
1305
 
1371
1306
  /**
1372
- * Deserializa un item individual
1373
- * @param {Object} rawItem - Item crudo de la base de datos
1374
- * @returns {Object} Item deserializado
1375
- */
1307
+ * Deserializes a single item
1308
+ * @param {Object} rawItem - Raw item from the database
1309
+ * @returns {Object} Deserialized item
1310
+ */
1376
1311
  deserializeItem(rawItem) {
1377
1312
  const deserializationResult = this.serializationManager.deserialize(rawItem.items);
1378
1313
  const deserializedItems = this.serializationManager.getData(deserializationResult, []);
@@ -1386,8 +1321,8 @@ class StorageManager {
1386
1321
  }
1387
1322
 
1388
1323
  /**
1389
- * RetryLogicManager - Maneja la lógica de reintentos y limpieza
1390
- * Responsabilidad única: Gestionar reintentos y limpieza de items fallidos
1324
+ * RetryLogicManager - Handles retry logic and cleanup
1325
+ * Single responsibility: Manage retries and cleanup of failed items
1391
1326
  */
1392
1327
  class RetryLogicManager {
1393
1328
  constructor(storageManager, configManager) {
@@ -1396,13 +1331,13 @@ class RetryLogicManager {
1396
1331
  }
1397
1332
 
1398
1333
  /**
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
1334
+ * Attempts to send failed items from the persistent buffer
1335
+ * @param {Function} sendCallback - Callback to send items
1336
+ * @param {Function} removeCallback - Callback to remove successful items
1402
1337
  */
1403
1338
  async retryFailedItems(sendCallback, removeCallback) {
1404
1339
  if (!this.storageManager) {
1405
- console.warn('SyntropyFront: Storage manager no disponible');
1340
+ console.warn('SyntropyFront: Storage manager not available');
1406
1341
  return;
1407
1342
  }
1408
1343
 
@@ -1411,7 +1346,7 @@ class RetryLogicManager {
1411
1346
 
1412
1347
  for (const item of failedItems) {
1413
1348
  if (item.retryCount < this.config.maxRetries) {
1414
- // Deserializar items del buffer
1349
+ // Deserialize items from buffer
1415
1350
  let deserializedItems;
1416
1351
  try {
1417
1352
  if (typeof item.items === 'string') {
@@ -1420,7 +1355,7 @@ class RetryLogicManager {
1420
1355
  deserializedItems = item.items;
1421
1356
  }
1422
1357
  } catch (error) {
1423
- console.error('SyntropyFront: Error deserializando items del buffer:', error);
1358
+ console.error('SyntropyFront: Error deserializing buffer items:', error);
1424
1359
  await this.removeFailedItem(item.id);
1425
1360
  continue;
1426
1361
  }
@@ -1429,35 +1364,35 @@ class RetryLogicManager {
1429
1364
  try {
1430
1365
  await sendCallback(deserializedItems, item.retryCount + 1, item.id);
1431
1366
 
1432
- // Si el envío fue exitoso, remover del buffer
1367
+ // On successful send, remove from buffer
1433
1368
  if (removeCallback) {
1434
1369
  await removeCallback(item.id);
1435
1370
  } else {
1436
1371
  await this.removeFailedItem(item.id);
1437
1372
  }
1438
1373
 
1439
- console.log(`SyntropyFront: Reintento exitoso para item ${item.id}`);
1374
+ console.log(`SyntropyFront: Retry successful for item ${item.id}`);
1440
1375
  } catch (error) {
1441
- console.warn(`SyntropyFront: Reintento falló para item ${item.id}:`, error);
1376
+ console.warn(`SyntropyFront: Retry failed for item ${item.id}:`, error);
1442
1377
 
1443
- // Incrementar contador de reintentos
1378
+ // Increment retry count
1444
1379
  await this.incrementRetryCount(item.id);
1445
1380
  }
1446
1381
  }
1447
1382
  } else {
1448
- console.warn(`SyntropyFront: Item ${item.id} excedió máximo de reintentos, removiendo del buffer`);
1383
+ console.warn(`SyntropyFront: Item ${item.id} exceeded maximum retries, removing from buffer`);
1449
1384
  await this.removeFailedItem(item.id);
1450
1385
  }
1451
1386
  }
1452
1387
  } catch (error) {
1453
- console.error('SyntropyFront: Error procesando reintentos:', error);
1388
+ console.error('SyntropyFront: Error processing retries:', error);
1454
1389
  }
1455
1390
  }
1456
1391
 
1457
1392
  /**
1458
- * Incrementa el contador de reintentos de un item
1459
- * @param {number} id - ID del item
1460
- */
1393
+ * Increments the retry count for an item
1394
+ * @param {number} id - Item ID
1395
+ */
1461
1396
  async incrementRetryCount(id) {
1462
1397
  try {
1463
1398
  const currentItem = await this.storageManager.retrieveById(id);
@@ -1467,25 +1402,25 @@ class RetryLogicManager {
1467
1402
  });
1468
1403
  }
1469
1404
  } catch (error) {
1470
- console.error('SyntropyFront: Error incrementando contador de reintentos:', error);
1405
+ console.error('SyntropyFront: Error incrementing retry count:', error);
1471
1406
  }
1472
1407
  }
1473
1408
 
1474
1409
  /**
1475
- * Remueve un item fallido del buffer
1476
- * @param {number} id - ID del item
1477
- */
1410
+ * Removes a failed item from the buffer
1411
+ * @param {number} id - Item ID
1412
+ */
1478
1413
  async removeFailedItem(id) {
1479
1414
  try {
1480
1415
  await this.storageManager.remove(id);
1481
1416
  } catch (error) {
1482
- console.error('SyntropyFront: Error removiendo item fallido:', error);
1417
+ console.error('SyntropyFront: Error removing failed item:', error);
1483
1418
  }
1484
1419
  }
1485
1420
 
1486
1421
  /**
1487
- * Limpia items que han excedido el máximo de reintentos
1488
- */
1422
+ * Cleans items that have exceeded the maximum retry count
1423
+ */
1489
1424
  async cleanupExpiredItems() {
1490
1425
  try {
1491
1426
  const allItems = await this.storageManager.retrieve();
@@ -1493,20 +1428,20 @@ class RetryLogicManager {
1493
1428
 
1494
1429
  for (const item of expiredItems) {
1495
1430
  await this.removeFailedItem(item.id);
1496
- console.warn(`SyntropyFront: Item ${item.id} removido por exceder máximo de reintentos`);
1431
+ console.warn(`SyntropyFront: Item ${item.id} removed for exceeding maximum retries`);
1497
1432
  }
1498
1433
 
1499
1434
  if (expiredItems.length > 0) {
1500
- console.log(`SyntropyFront: Limpieza completada, ${expiredItems.length} items removidos`);
1435
+ console.log(`SyntropyFront: Cleanup completed, ${expiredItems.length} items removed`);
1501
1436
  }
1502
1437
  } catch (error) {
1503
- console.error('SyntropyFront: Error en limpieza de items expirados:', error);
1438
+ console.error('SyntropyFront: Error cleaning up expired items:', error);
1504
1439
  }
1505
1440
  }
1506
1441
 
1507
1442
  /**
1508
- * Obtiene estadísticas de reintentos
1509
- */
1443
+ * Returns retry statistics
1444
+ */
1510
1445
  async getRetryStats() {
1511
1446
  try {
1512
1447
  const allItems = await this.storageManager.retrieve();
@@ -1529,7 +1464,7 @@ class RetryLogicManager {
1529
1464
 
1530
1465
  return stats;
1531
1466
  } catch (error) {
1532
- console.error('SyntropyFront: Error obteniendo estadísticas de reintentos:', error);
1467
+ console.error('SyntropyFront: Error getting retry statistics:', error);
1533
1468
  return {
1534
1469
  totalItems: 0,
1535
1470
  itemsByRetryCount: {},
@@ -1540,77 +1475,253 @@ class RetryLogicManager {
1540
1475
  }
1541
1476
 
1542
1477
  /**
1543
- * SerializationManager - Maneja la serialización y deserialización de datos
1544
- * Responsabilidad única: Gestionar la transformación de datos para almacenamiento
1478
+ * Functional Fragments: Pure masking strategies.
1479
+ */
1480
+ const MASKING_STRATEGIES = {
1481
+ credit_card: (value, rule) => {
1482
+ const clean = value.replace(/\D/g, '');
1483
+ if (rule.preserveLength) {
1484
+ return value.replace(/\d/g, (match, offset) => {
1485
+ const digitIndex = value.substring(0, offset).replace(/\D/g, '').length;
1486
+ return digitIndex < clean.length - 4 ? rule.maskChar : match;
1487
+ });
1488
+ }
1489
+ return `${rule.maskChar.repeat(4)}-${rule.maskChar.repeat(4)}-${rule.maskChar.repeat(4)}-${clean.slice(-4)}`;
1490
+ },
1491
+
1492
+ ssn: (value, rule) => {
1493
+ const clean = value.replace(/\D/g, '');
1494
+ if (rule.preserveLength) {
1495
+ return value.replace(/\d/g, (match, offset) => {
1496
+ const digitIndex = value.substring(0, offset).replace(/\D/g, '').length;
1497
+ return digitIndex < clean.length - 4 ? rule.maskChar : match;
1498
+ });
1499
+ }
1500
+ return `***-**-${clean.slice(-4)}`;
1501
+ },
1502
+
1503
+ email: (value, rule) => {
1504
+ const atIndex = value.indexOf('@');
1505
+ if (atIndex <= 0) return MASKING_STRATEGIES.default(value, rule);
1506
+
1507
+ const username = value.substring(0, atIndex);
1508
+ const domain = value.substring(atIndex);
1509
+
1510
+ if (rule.preserveLength) {
1511
+ const maskedUsername = username.length > 1
1512
+ ? username.charAt(0) + rule.maskChar.repeat(username.length - 1)
1513
+ : rule.maskChar.repeat(username.length);
1514
+ return maskedUsername + domain;
1515
+ }
1516
+ return `${username.charAt(0)}***${domain}`;
1517
+ },
1518
+
1519
+ phone: (value, rule) => {
1520
+ const clean = value.replace(/\D/g, '');
1521
+ if (rule.preserveLength) {
1522
+ return value.replace(/\d/g, (match, offset) => {
1523
+ const digitIndex = value.substring(0, offset).replace(/\D/g, '').length;
1524
+ return digitIndex < clean.length - 4 ? rule.maskChar : match;
1525
+ });
1526
+ }
1527
+ return `${rule.maskChar.repeat(3)}-${rule.maskChar.repeat(3)}-${clean.slice(-4)}`;
1528
+ },
1529
+
1530
+ secret: (value, rule) => {
1531
+ if (rule.preserveLength) return rule.maskChar.repeat(value.length);
1532
+ return rule.maskChar.repeat(8);
1533
+ },
1534
+
1535
+ custom: (value, rule) => {
1536
+ return typeof rule.customMask === 'function' ? rule.customMask(value) : value;
1537
+ },
1538
+
1539
+ default: (value, rule) => {
1540
+ if (rule.preserveLength) return rule.maskChar.repeat(value.length);
1541
+ return rule.maskChar.repeat(Math.min(value.length, 8));
1542
+ }
1543
+ };
1544
+
1545
+ // Compatibility Aliases
1546
+ MASKING_STRATEGIES.password = MASKING_STRATEGIES.secret;
1547
+ MASKING_STRATEGIES.token = MASKING_STRATEGIES.secret;
1548
+
1549
+ const MaskingStrategy = {
1550
+ CREDIT_CARD: 'credit_card',
1551
+ SSN: 'ssn',
1552
+ EMAIL: 'email',
1553
+ PHONE: 'phone',
1554
+ PASSWORD: 'password'};
1555
+
1556
+ /**
1557
+ * DataMaskingManager - PII Obfuscation Engine.
1558
+ * Implements Strategy Pattern (SOLID: OCP).
1559
+ */
1560
+ class DataMaskingManager {
1561
+ constructor(options = {}) {
1562
+ this.maskChar = options.maskChar || '*';
1563
+ this.preserveLength = options.preserveLength !== false;
1564
+ this.rules = [];
1565
+ this.strategies = new Map(Object.entries(MASKING_STRATEGIES));
1566
+
1567
+ // ANSI escape code regex (intentional control chars for stripping terminal codes)
1568
+ // eslint-disable-next-line no-control-regex
1569
+ this.ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
1570
+
1571
+ if (options.enableDefaultRules !== false) {
1572
+ this.addDefaultRules();
1573
+ }
1574
+
1575
+ if (options.rules) {
1576
+ options.rules.forEach(rule => this.addRule(rule));
1577
+ }
1578
+ }
1579
+
1580
+ addDefaultRules() {
1581
+ const defaultRules = [
1582
+ { pattern: /credit_card|card_number|payment_number/i, strategy: MaskingStrategy.CREDIT_CARD },
1583
+ { pattern: /ssn|social_security|security_number/i, strategy: MaskingStrategy.SSN },
1584
+ { pattern: /email/i, strategy: MaskingStrategy.EMAIL },
1585
+ { pattern: /phone|phone_number|mobile_number/i, strategy: MaskingStrategy.PHONE },
1586
+ { pattern: /password|pass|pwd|secret|api_key|token|auth_token|jwt|bearer/i, strategy: MaskingStrategy.PASSWORD }
1587
+ ];
1588
+
1589
+ defaultRules.forEach(rule => this.addRule(rule));
1590
+ }
1591
+
1592
+ addRule(rule) {
1593
+ this.rules.push({
1594
+ ...rule,
1595
+ _compiledPattern: typeof rule.pattern === 'string' ? new RegExp(rule.pattern, 'i') : rule.pattern,
1596
+ preserveLength: rule.preserveLength ?? this.preserveLength,
1597
+ maskChar: rule.maskChar ?? this.maskChar
1598
+ });
1599
+ }
1600
+
1601
+ registerStrategy(name, strategyFn) {
1602
+ if (typeof strategyFn === 'function') {
1603
+ this.strategies.set(name, strategyFn);
1604
+ }
1605
+ }
1606
+
1607
+ process(data) {
1608
+ if (data === null || data === undefined) return data;
1609
+
1610
+ const processors = {
1611
+ string: (val) => val.replace(this.ansiRegex, ''),
1612
+ object: (val) => {
1613
+ if (Array.isArray(val)) return val.map(item => this.process(item));
1614
+ if (val && val.constructor === Object) return this.maskObject(val);
1615
+ return val;
1616
+ }
1617
+ };
1618
+
1619
+ return (processors[typeof data] || ((v) => v))(data);
1620
+ }
1621
+
1622
+ maskObject(data) {
1623
+ return Object.entries(data).reduce((acc, [key, value]) => {
1624
+ acc[key] = this.maskValue(key, value);
1625
+ return acc;
1626
+ }, {});
1627
+ }
1628
+
1629
+ maskValue(key, value) {
1630
+ if (typeof value !== 'string') return this.process(value);
1631
+
1632
+ const rule = this.rules.find(r => r._compiledPattern?.test(key));
1633
+ return rule ? this.applyStrategy(value, rule) : value.replace(this.ansiRegex, '');
1634
+ }
1635
+
1636
+ applyStrategy(value, rule) {
1637
+ const strategy = this.strategies.get(rule.strategy) || this.strategies.get('default');
1638
+ return strategy(value, rule);
1639
+ }
1640
+ }
1641
+
1642
+ const dataMaskingManager = new DataMaskingManager();
1643
+
1644
+ /**
1645
+ * SerializationManager - Handles serialization and deserialization of data.
1646
+ * Single responsibility: Manage data transformation for storage.
1647
+ * DIP: Accepts serializer and masking via constructor for testability and substitution.
1648
+ * @param {Object} [deps] - Injected dependencies
1649
+ * @param {{ serialize: function(*): string, deserialize: function(string): * }} [deps.serializer] - Serializer (circular ref handling)
1650
+ * @param {{ process: function(*): * }} [deps.masking] - Object with process(data) for PII obfuscation
1545
1651
  */
1546
1652
  class SerializationManager {
1547
- constructor() {
1548
- this.serializer = robustSerializer;
1653
+ constructor(deps = {}) {
1654
+ this.serializer = deps.serializer ?? robustSerializer;
1655
+ this.masking = deps.masking ?? dataMaskingManager;
1549
1656
  }
1550
1657
 
1551
1658
  /**
1552
- * Serializa items con manejo declarativo de errores
1553
- * @param {Array} items - Items a serializar
1554
- * @returns {Object} Resultado de serialización
1659
+ * Serializes items with declarative error handling
1660
+ * @param {Array} items - Items to serialize
1661
+ * @returns {Object} Serialization result
1555
1662
  */
1556
1663
  serialize(items) {
1557
- const serializationResult = {
1558
- success: false,
1559
- data: null,
1560
- error: null,
1561
- timestamp: new Date().toISOString()
1562
- };
1563
-
1564
1664
  try {
1565
- const serializedData = this.serializer.serialize(items);
1665
+ // Guard: Validate basic input
1666
+ if (items === null || items === undefined) {
1667
+ return { success: true, data: this.serializer.serialize([]), error: null, timestamp: new Date().toISOString() };
1668
+ }
1669
+
1670
+ // Apply masking before store/send
1671
+ const maskedItems = this.masking.process(items);
1672
+ const serializedData = this.serializer.serialize(maskedItems);
1673
+
1566
1674
  return {
1567
- ...serializationResult,
1568
1675
  success: true,
1569
- data: serializedData
1676
+ data: serializedData,
1677
+ error: null,
1678
+ timestamp: new Date().toISOString()
1570
1679
  };
1571
1680
  } catch (error) {
1572
1681
  return {
1573
- ...serializationResult,
1682
+ success: false,
1683
+ data: this.createFallbackData(error),
1574
1684
  error: this.createSerializationError(error),
1575
- data: this.createFallbackData(error)
1685
+ timestamp: new Date().toISOString()
1576
1686
  };
1577
1687
  }
1578
1688
  }
1579
1689
 
1580
1690
  /**
1581
- * Deserializa datos con manejo declarativo de errores
1691
+ * Deserializes data with declarative error handling
1582
1692
  * @param {string} serializedData - Datos serializados
1583
- * @returns {Object} Resultado de deserialización
1693
+ * @returns {Object} Deserialization result
1584
1694
  */
1585
1695
  deserialize(serializedData) {
1586
- const deserializationResult = {
1587
- success: false,
1588
- data: null,
1589
- error: null,
1590
- timestamp: new Date().toISOString()
1591
- };
1592
-
1593
1696
  try {
1697
+ // Guard: Empty or null data
1698
+ if (!serializedData) {
1699
+ return { success: true, data: [], error: null, timestamp: new Date().toISOString() };
1700
+ }
1701
+
1594
1702
  const deserializedData = this.serializer.deserialize(serializedData);
1703
+
1595
1704
  return {
1596
- ...deserializationResult,
1597
1705
  success: true,
1598
- data: deserializedData
1706
+ data: deserializedData,
1707
+ error: null,
1708
+ timestamp: new Date().toISOString()
1599
1709
  };
1600
1710
  } catch (error) {
1601
1711
  return {
1602
- ...deserializationResult,
1712
+ success: false,
1713
+ data: [],
1603
1714
  error: this.createDeserializationError(error),
1604
- data: []
1715
+ timestamp: new Date().toISOString()
1605
1716
  };
1606
1717
  }
1607
1718
  }
1608
1719
 
1609
1720
  /**
1610
- * Crea un error de serialización estructurado
1611
- * @param {Error} error - Error original
1612
- * @returns {Object} Error estructurado
1613
- */
1721
+ * Creates a structured serialization error
1722
+ * @param {Error} error - Original error
1723
+ * @returns {Object} Structured error
1724
+ */
1614
1725
  createSerializationError(error) {
1615
1726
  return {
1616
1727
  type: 'serialization_error',
@@ -1621,10 +1732,10 @@ class SerializationManager {
1621
1732
  }
1622
1733
 
1623
1734
  /**
1624
- * Crea un error de deserialización estructurado
1625
- * @param {Error} error - Error original
1626
- * @returns {Object} Error estructurado
1627
- */
1735
+ * Creates a structured deserialization error
1736
+ * @param {Error} error - Original error
1737
+ * @returns {Object} Structured error
1738
+ */
1628
1739
  createDeserializationError(error) {
1629
1740
  return {
1630
1741
  type: 'deserialization_error',
@@ -1635,83 +1746,87 @@ class SerializationManager {
1635
1746
  }
1636
1747
 
1637
1748
  /**
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
1749
+ * Creates fallback data when serialization fails
1750
+ * @param {Error} error - Error that caused the fallback
1751
+ * @returns {string} Serialized fallback data
1641
1752
  */
1642
1753
  createFallbackData(error) {
1643
1754
  const fallbackPayload = {
1644
1755
  __serializationError: true,
1645
1756
  error: error.message,
1646
1757
  timestamp: new Date().toISOString(),
1647
- fallbackData: 'Items no serializables - usando fallback'
1758
+ fallbackData: 'Items not serializable - using fallback'
1648
1759
  };
1649
1760
 
1650
1761
  return JSON.stringify(fallbackPayload);
1651
1762
  }
1652
1763
 
1653
1764
  /**
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
- */
1765
+ * Checks if a serialization result was successful
1766
+ * @param {Object} result - Serialization/deserialization result
1767
+ * @returns {boolean} True if successful
1768
+ */
1658
1769
  isSuccessful(result) {
1659
1770
  return Boolean(result && result.success === true);
1660
1771
  }
1661
1772
 
1662
1773
  /**
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
1774
+ * Gets data from a result, with fallback
1775
+ * @param {Object} result - Serialization/deserialization result
1776
+ * @param {*} fallback - Default value if failed
1777
+ * @returns {*} Data or fallback
1667
1778
  */
1668
1779
  getData(result, fallback = null) {
1669
1780
  return this.isSuccessful(result) ? result.data : fallback;
1670
1781
  }
1671
1782
  }
1672
1783
 
1784
+ const DEFAULT_DB_NAME = 'SyntropyFrontBuffer';
1785
+ const DEFAULT_DB_VERSION = 1;
1786
+ const DEFAULT_STORE_NAME = 'failedItems';
1787
+
1673
1788
  /**
1674
- * PersistentBufferManager - Coordinador del buffer persistente
1675
- * Responsabilidad única: Coordinar los componentes de almacenamiento persistente
1789
+ * PersistentBufferManager - Persistent buffer coordinator
1790
+ * Single Responsibility: Coordinate persistent storage components
1791
+ * DIP: Accepts injected components (databaseManager, serializationManager, storageManager, retryLogicManager) for tests and substitution.
1792
+ * @param {Object} configManager - Config (usePersistentBuffer, maxRetries, etc.)
1793
+ * @param {Object} [deps] - Injected components; defaults created if not provided
1676
1794
  */
1677
1795
  class PersistentBufferManager {
1678
- constructor(configManager) {
1796
+ constructor(configManager, deps = {}) {
1679
1797
  this.config = configManager;
1680
1798
  this.usePersistentBuffer = false;
1681
-
1682
- // Inicializar componentes especializados
1683
- this.databaseManager = new DatabaseManager(
1684
- 'SyntropyFrontBuffer',
1685
- 1,
1686
- 'failedItems'
1799
+
1800
+ this.databaseManager = deps.databaseManager ?? new DatabaseManager(
1801
+ DEFAULT_DB_NAME,
1802
+ DEFAULT_DB_VERSION,
1803
+ DEFAULT_STORE_NAME
1687
1804
  );
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
1805
+ this.serializationManager = deps.serializationManager ?? new SerializationManager();
1806
+ this.storageManager = deps.storageManager ?? new StorageManager(this.databaseManager, this.serializationManager);
1807
+ this.retryLogicManager = deps.retryLogicManager ?? new RetryLogicManager(this.storageManager, this.config);
1808
+
1694
1809
  this.initPersistentBuffer();
1695
1810
  }
1696
1811
 
1697
1812
  /**
1698
- * Inicializa el buffer persistente
1813
+ * Initializes the persistent buffer
1699
1814
  */
1700
1815
  async initPersistentBuffer() {
1701
1816
  try {
1702
1817
  const success = await this.databaseManager.init();
1703
1818
  if (success) {
1704
1819
  this.usePersistentBuffer = this.config.usePersistentBuffer;
1705
- console.log('SyntropyFront: Buffer persistente inicializado');
1820
+ console.log('SyntropyFront: Persistent buffer initialized');
1706
1821
  }
1707
1822
  } catch (error) {
1708
- console.warn('SyntropyFront: Error inicializando buffer persistente:', error);
1823
+ console.warn('SyntropyFront: Error initializing persistent buffer:', error);
1709
1824
  }
1710
1825
  }
1711
1826
 
1712
1827
  /**
1713
- * Guarda items fallidos en el buffer persistente
1714
- * @param {Array} items - Items a guardar
1828
+ * Saves failed items to the persistent buffer
1829
+ * @param {Array} items - Items to save
1715
1830
  */
1716
1831
  async save(items) {
1717
1832
  if (!this.usePersistentBuffer) {
@@ -1720,14 +1835,14 @@ class PersistentBufferManager {
1720
1835
 
1721
1836
  try {
1722
1837
  await this.storageManager.save(items);
1723
- console.log('SyntropyFront: Items guardados en buffer persistente');
1838
+ console.log('SyntropyFront: Items saved to persistent buffer');
1724
1839
  } catch (error) {
1725
- console.error('SyntropyFront: Error guardando en buffer persistente:', error);
1840
+ console.error('SyntropyFront: Error saving to persistent buffer:', error);
1726
1841
  }
1727
1842
  }
1728
1843
 
1729
1844
  /**
1730
- * Obtiene items fallidos del buffer persistente
1845
+ * Retrieves failed items from the persistent buffer
1731
1846
  */
1732
1847
  async retrieve() {
1733
1848
  if (!this.usePersistentBuffer) {
@@ -1737,14 +1852,14 @@ class PersistentBufferManager {
1737
1852
  try {
1738
1853
  return await this.storageManager.retrieve();
1739
1854
  } catch (error) {
1740
- console.error('SyntropyFront: Error obteniendo del buffer persistente:', error);
1855
+ console.error('SyntropyFront: Error retrieving from persistent buffer:', error);
1741
1856
  return [];
1742
1857
  }
1743
1858
  }
1744
1859
 
1745
1860
  /**
1746
- * Remueve items del buffer persistente
1747
- * @param {number} id - ID del item a remover
1861
+ * Removes items from the persistent buffer
1862
+ * @param {number} id - ID of the item to remove
1748
1863
  */
1749
1864
  async remove(id) {
1750
1865
  if (!this.usePersistentBuffer) {
@@ -1754,14 +1869,14 @@ class PersistentBufferManager {
1754
1869
  try {
1755
1870
  await this.storageManager.remove(id);
1756
1871
  } catch (error) {
1757
- console.error('SyntropyFront: Error removiendo del buffer persistente:', error);
1872
+ console.error('SyntropyFront: Error removing from persistent buffer:', error);
1758
1873
  }
1759
1874
  }
1760
1875
 
1761
1876
  /**
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
1877
+ * Attempts to send failed items from the persistent buffer
1878
+ * @param {Function} sendCallback - Callback to send items
1879
+ * @param {Function} removeCallback - Callback to remove successful items
1765
1880
  */
1766
1881
  async retryFailedItems(sendCallback, removeCallback) {
1767
1882
  if (!this.usePersistentBuffer) {
@@ -1772,7 +1887,7 @@ class PersistentBufferManager {
1772
1887
  }
1773
1888
 
1774
1889
  /**
1775
- * Limpia items que han excedido el máximo de reintentos
1890
+ * Cleans up items that have exceeded the maximum retry count
1776
1891
  */
1777
1892
  async cleanupExpiredItems() {
1778
1893
  if (!this.usePersistentBuffer) {
@@ -1783,7 +1898,7 @@ class PersistentBufferManager {
1783
1898
  }
1784
1899
 
1785
1900
  /**
1786
- * Obtiene estadísticas del buffer persistente
1901
+ * Gets persistent buffer statistics
1787
1902
  */
1788
1903
  async getStats() {
1789
1904
  if (!this.usePersistentBuffer) {
@@ -1802,7 +1917,7 @@ class PersistentBufferManager {
1802
1917
  isAvailable: this.isAvailable()
1803
1918
  };
1804
1919
  } catch (error) {
1805
- console.error('SyntropyFront: Error obteniendo estadísticas:', error);
1920
+ console.error('SyntropyFront: Error getting statistics:', error);
1806
1921
  return {
1807
1922
  totalItems: 0,
1808
1923
  itemsByRetryCount: {},
@@ -1813,14 +1928,14 @@ class PersistentBufferManager {
1813
1928
  }
1814
1929
 
1815
1930
  /**
1816
- * Verifica si el buffer persistente está disponible
1931
+ * Checks if the persistent buffer is available
1817
1932
  */
1818
1933
  isAvailable() {
1819
1934
  return this.usePersistentBuffer && this.databaseManager.isDatabaseAvailable();
1820
1935
  }
1821
1936
 
1822
1937
  /**
1823
- * Limpia todo el buffer persistente
1938
+ * Clears the entire persistent buffer
1824
1939
  */
1825
1940
  async clear() {
1826
1941
  if (!this.usePersistentBuffer) {
@@ -1829,14 +1944,14 @@ class PersistentBufferManager {
1829
1944
 
1830
1945
  try {
1831
1946
  await this.storageManager.clear();
1832
- console.log('SyntropyFront: Buffer persistente limpiado');
1947
+ console.log('SyntropyFront: Persistent buffer cleared');
1833
1948
  } catch (error) {
1834
- console.error('SyntropyFront: Error limpiando buffer persistente:', error);
1949
+ console.error('SyntropyFront: Error clearing persistent buffer:', error);
1835
1950
  }
1836
1951
  }
1837
1952
 
1838
1953
  /**
1839
- * Cierra la conexión con la base de datos
1954
+ * Closes the database connection
1840
1955
  */
1841
1956
  close() {
1842
1957
  this.databaseManager.close();
@@ -1845,60 +1960,56 @@ class PersistentBufferManager {
1845
1960
  }
1846
1961
 
1847
1962
  /**
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
1963
+ * Agent - Coordinates sending traceability data to the backend.
1964
+ * DIP: All dependencies (config, queue, retry, transport, buffer, masking) are injectable.
1855
1965
  *
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
1966
+ * @contract
1967
+ * - configure(config): updates agent configuration.
1968
+ * - sendError(payload, context?): if enabled and not dropped by sampling, enqueues an item of type 'error'.
1969
+ * - sendBreadcrumbs(breadcrumbs): if enabled, batchTimeout set and non-empty, enqueues type 'breadcrumbs'.
1970
+ * - flush(): drains queue via flushCallback. forceFlush(): flush + processes pending retries.
1971
+ * - getStats(): returns { queueLength, retryQueueLength, isEnabled, usePersistentBuffer, maxRetries }.
1867
1972
  */
1868
1973
  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
1974
+ /**
1975
+ * @param {Object} [deps] - Injected dependencies (config, queue, retry, transport, buffer, masking). Defaults created if not provided.
1976
+ */
1977
+ constructor(deps = {}) {
1978
+ // Components (Injected or Defaults)
1979
+ this.config = deps.config || new ConfigurationManager();
1980
+ this.masking = deps.masking || dataMaskingManager;
1981
+
1982
+ // Managers that depend on config
1983
+ this.queue = deps.queue || new QueueManager(this.config);
1984
+ this.retry = deps.retry || new RetryManager(this.config);
1985
+ this.transport = deps.transport || new HttpTransport(this.config);
1986
+ this.buffer = deps.buffer || new PersistentBufferManager(this.config);
1987
+
1988
+ // Setup coordination callbacks
1878
1989
  this.setupCallbacks();
1879
1990
  }
1880
1991
 
1881
1992
  /**
1882
- * Configura callbacks para coordinación entre componentes
1883
- */
1993
+ * Configures callbacks for coordination between components
1994
+ */
1884
1995
  setupCallbacks() {
1885
- // Callback para el QueueManager cuando hace flush
1996
+ // QueueManager flush callback
1886
1997
  this.queue.flushCallback = async (items) => {
1887
1998
  try {
1888
1999
  await this.transport.send(items);
1889
- console.log('SyntropyFront: Datos enviados exitosamente');
2000
+ console.log('SyntropyFront: Data sent successfully');
1890
2001
  } catch (error) {
1891
- console.error('SyntropyFront Agent: Error enviando datos:', error);
1892
-
1893
- // Agregar a cola de reintentos
2002
+ console.error('SyntropyFront Agent: Error sending data:', error);
2003
+
2004
+ // Add to retry queue
1894
2005
  this.retry.addToRetryQueue(items);
1895
-
1896
- // Guardar en buffer persistente
2006
+
2007
+ // Save to persistent buffer
1897
2008
  await this.buffer.save(items);
1898
2009
  }
1899
2010
  };
1900
2011
 
1901
- // Callback para el RetryManager cuando procesa reintentos
2012
+ // RetryManager send callback
1902
2013
  this.retry.sendCallback = async (items) => {
1903
2014
  return await this.transport.send(items);
1904
2015
  };
@@ -1907,90 +2018,95 @@ class Agent {
1907
2018
  await this.buffer.remove(id);
1908
2019
  };
1909
2020
 
1910
- // Callback para el PersistentBufferManager cuando retry items
2021
+ // PersistentBufferManager retry callback
1911
2022
  this.buffer.sendCallback = (items, retryCount, persistentId) => {
1912
2023
  this.retry.addToRetryQueue(items, retryCount, persistentId);
1913
2024
  };
1914
2025
  }
1915
2026
 
1916
-
1917
-
1918
2027
  /**
1919
- * Configura el agent
1920
- * @param {Object} config - Configuración del agent
1921
- */
2028
+ * Updates configuration (endpoint, headers, batchSize, batchTimeout, samplingRate, etc.).
2029
+ * @param {Object} config - Ver ConfigurationManager.configure
2030
+ * @returns {void}
2031
+ */
1922
2032
  configure(config) {
1923
2033
  this.config.configure(config);
1924
2034
  }
1925
2035
 
1926
2036
  /**
1927
- * Envía un error al backend
1928
- * @param {Object} errorPayload - Payload del error
1929
- * @param {Object} context - Contexto adicional (opcional)
1930
- */
2037
+ * Enqueues an error for send. Does not send if agent is disabled or sampling discards it.
2038
+ * @param {Object} errorPayload - Payload with type, error, breadcrumbs, timestamp
2039
+ * @param {Object|null} [context=null] - Additional context (merged into payload)
2040
+ * @returns {void}
2041
+ */
1931
2042
  sendError(errorPayload, context = null) {
2043
+ // 🛡️ Guard: Agent enabled
1932
2044
  if (!this.config.isAgentEnabled()) {
1933
- console.warn('SyntropyFront Agent: No configurado, error no enviado');
2045
+ console.warn('SyntropyFront Agent: Not configured, error not sent');
1934
2046
  return;
1935
2047
  }
1936
2048
 
1937
- // Agregar contexto si está disponible
1938
- const payloadWithContext = context ? {
1939
- ...errorPayload,
1940
- context
1941
- } : errorPayload;
2049
+ // 🎲 Guard: Sampling
2050
+ if (Math.random() > this.config.samplingRate) return;
1942
2051
 
1943
- // Aplicar encriptación si está configurada
2052
+ // Functional Pipeline: Generate payload with context
2053
+ const payloadWithContext = context
2054
+ ? { ...errorPayload, context }
2055
+ : errorPayload;
2056
+
2057
+ // Apply transformations (Encryption and Obfuscation)
1944
2058
  const dataToSend = this.transport.applyEncryption(payloadWithContext);
2059
+ const maskedData = this.masking.process(dataToSend);
1945
2060
 
1946
2061
  this.queue.add({
1947
2062
  type: 'error',
1948
- data: dataToSend,
2063
+ data: maskedData,
1949
2064
  timestamp: new Date().toISOString()
1950
2065
  });
1951
2066
  }
1952
2067
 
1953
2068
  /**
1954
- * Envía breadcrumbs al backend
1955
- * @param {Array} breadcrumbs - Lista de breadcrumbs
1956
- */
2069
+ * Enqueues breadcrumbs for send. Does not send if disabled, no batchTimeout, or empty array.
2070
+ * @param {Array<Object>} breadcrumbs - List of traces (category, message, data, timestamp)
2071
+ * @returns {void}
2072
+ */
1957
2073
  sendBreadcrumbs(breadcrumbs) {
1958
- // Solo enviar breadcrumbs si está habilitado (batchTimeout configurado)
2074
+ // 🛡️ Guard: Enabled and with data
1959
2075
  if (!this.config.isAgentEnabled() || !this.config.shouldSendBreadcrumbs() || !breadcrumbs.length) {
1960
2076
  return;
1961
2077
  }
1962
2078
 
1963
- // Aplicar encriptación si está configurada
2079
+ // 🎲 Guard: Sampling
2080
+ if (Math.random() > this.config.samplingRate) return;
2081
+
2082
+ // Apply transformations
1964
2083
  const dataToSend = this.transport.applyEncryption(breadcrumbs);
2084
+ const maskedData = this.masking.process(dataToSend);
1965
2085
 
1966
2086
  this.queue.add({
1967
2087
  type: 'breadcrumbs',
1968
- data: dataToSend,
2088
+ data: maskedData,
1969
2089
  timestamp: new Date().toISOString()
1970
2090
  });
1971
2091
  }
1972
2092
 
1973
2093
  /**
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
- */
2094
+ * Adds an item to the queue (compatibility)
2095
+ */
1977
2096
  addToQueue(item) {
1978
2097
  this.queue.add(item);
1979
2098
  }
1980
2099
 
1981
2100
  /**
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
- */
2101
+ * Adds to retry queue (compatibility)
2102
+ */
1987
2103
  addToRetryQueue(items, retryCount = 1, persistentId = null) {
1988
2104
  this.retry.addToRetryQueue(items, retryCount, persistentId);
1989
2105
  }
1990
2106
 
1991
2107
  /**
1992
- * Procesa la cola de reintentos (método público para compatibilidad)
1993
- */
2108
+ * Processes retry queue (compatibility)
2109
+ */
1994
2110
  async processRetryQueue() {
1995
2111
  await this.retry.processRetryQueue(
1996
2112
  this.retry.sendCallback,
@@ -1999,29 +2115,28 @@ class Agent {
1999
2115
  }
2000
2116
 
2001
2117
  /**
2002
- * Envía todos los items en cola
2003
- */
2118
+ * Forces flush of current queue
2119
+ */
2004
2120
  async flush() {
2005
2121
  await this.queue.flush(this.queue.flushCallback);
2006
2122
  }
2007
2123
 
2008
2124
  /**
2009
- * Fuerza el envío inmediato de todos los datos pendientes
2010
- */
2125
+ * Forces immediate flush of everything
2126
+ */
2011
2127
  async forceFlush() {
2012
2128
  await this.flush();
2013
-
2014
- // También intentar enviar items en cola de reintentos
2129
+
2130
+ // Also attempt pending retries
2015
2131
  if (!this.retry.isEmpty()) {
2016
- console.log('SyntropyFront: Intentando enviar items en cola de reintentos...');
2132
+ console.log('SyntropyFront: Attempting to send pending retries...');
2017
2133
  await this.processRetryQueue();
2018
2134
  }
2019
2135
  }
2020
2136
 
2021
2137
  /**
2022
- * Obtiene estadísticas del agent
2023
- * @returns {Object} Estadísticas
2024
- */
2138
+ * Gets agent stats
2139
+ */
2025
2140
  getStats() {
2026
2141
  const config = this.config.getConfig();
2027
2142
  return {
@@ -2034,670 +2149,770 @@ class Agent {
2034
2149
  }
2035
2150
 
2036
2151
  /**
2037
- * Intenta enviar items fallidos del buffer persistente
2038
- */
2152
+ * Attempts to send failed items from persistent buffer
2153
+ */
2039
2154
  async retryFailedItems() {
2040
2155
  await this.buffer.retryFailedItems(this.buffer.sendCallback);
2041
2156
  }
2042
2157
 
2043
2158
  /**
2044
- * Desactiva el agent
2045
- */
2159
+ * Returns whether the agent is enabled (for consumers that need to check without depending on config shape).
2160
+ */
2161
+ isEnabled() {
2162
+ return this.config.isAgentEnabled();
2163
+ }
2164
+
2165
+ /**
2166
+ * Returns whether breadcrumbs should be sent (e.g. batch mode with timeout).
2167
+ */
2168
+ shouldSendBreadcrumbs() {
2169
+ return this.config.shouldSendBreadcrumbs();
2170
+ }
2171
+
2172
+ /**
2173
+ * Disables the agent
2174
+ */
2046
2175
  disable() {
2047
- this.config.configure({ endpoint: null }); // Deshabilitar
2176
+ this.config.configure({ endpoint: null });
2048
2177
  this.queue.clear();
2049
2178
  this.retry.clear();
2050
2179
  }
2051
2180
  }
2052
2181
 
2053
- // Instancia singleton
2182
+ // Singleton Instance
2054
2183
  const agent = new Agent();
2055
2184
 
2056
2185
  /**
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
2186
+ * Environment - Centralized detection of browser environment and capabilities.
2187
+ * Browser-only: targets modern browsers per browserslist config.
2061
2188
  */
2062
- class ContextCollector {
2063
- constructor() {
2064
- // Sets curados por defecto (seguros y útiles)
2065
- this.defaultContexts = {
2066
- device: {
2067
- userAgent: () => navigator.userAgent,
2068
- language: () => navigator.language,
2069
- screen: () => ({
2070
- width: window.screen.width,
2071
- height: window.screen.height
2072
- }),
2073
- timezone: () => Intl.DateTimeFormat().resolvedOptions().timeZone
2074
- },
2075
- window: {
2076
- url: () => window.location.href,
2077
- viewport: () => ({
2078
- width: window.innerWidth,
2079
- height: window.innerHeight
2080
- }),
2081
- title: () => document.title
2082
- },
2083
- session: {
2084
- sessionId: () => this.generateSessionId(),
2085
- pageLoadTime: () => performance.now()
2086
- },
2087
- ui: {
2088
- visibility: () => document.visibilityState,
2089
- activeElement: () => document.activeElement ? {
2090
- tagName: document.activeElement.tagName
2091
- } : null
2092
- },
2093
- network: {
2094
- online: () => navigator.onLine,
2095
- connection: () => navigator.connection ? {
2096
- effectiveType: navigator.connection.effectiveType
2097
- } : null
2098
- }
2099
- };
2100
-
2101
- // Mapeo completo de todos los campos disponibles
2102
- this.allFields = {
2103
- device: {
2104
- userAgent: () => navigator.userAgent,
2105
- language: () => navigator.language,
2106
- languages: () => navigator.languages,
2107
- screen: () => ({
2108
- width: window.screen.width,
2109
- height: window.screen.height,
2110
- availWidth: window.screen.availWidth,
2111
- availHeight: window.screen.availHeight,
2112
- colorDepth: window.screen.colorDepth,
2113
- pixelDepth: window.screen.pixelDepth
2114
- }),
2115
- timezone: () => Intl.DateTimeFormat().resolvedOptions().timeZone,
2116
- cookieEnabled: () => navigator.cookieEnabled,
2117
- doNotTrack: () => navigator.doNotTrack
2118
- },
2119
- window: {
2120
- url: () => window.location.href,
2121
- pathname: () => window.location.pathname,
2122
- search: () => window.location.search,
2123
- hash: () => window.location.hash,
2124
- referrer: () => document.referrer,
2125
- title: () => document.title,
2126
- viewport: () => ({
2127
- width: window.innerWidth,
2128
- height: window.innerHeight
2129
- })
2130
- },
2131
- storage: {
2132
- localStorage: () => {
2133
- const keys = Object.keys(localStorage);
2134
- return {
2135
- keys: keys.length,
2136
- size: JSON.stringify(localStorage).length,
2137
- keyNames: keys // Solo nombres, no valores
2138
- };
2139
- },
2140
- sessionStorage: () => {
2141
- const keys = Object.keys(sessionStorage);
2142
- return {
2143
- keys: keys.length,
2144
- size: JSON.stringify(sessionStorage).length,
2145
- keyNames: keys // Solo nombres, no valores
2146
- };
2147
- }
2148
- },
2149
- network: {
2150
- online: () => navigator.onLine,
2151
- connection: () => navigator.connection ? {
2152
- effectiveType: navigator.connection.effectiveType,
2153
- downlink: navigator.connection.downlink,
2154
- rtt: navigator.connection.rtt
2155
- } : null
2156
- },
2157
- ui: {
2158
- focused: () => document.hasFocus(),
2159
- visibility: () => document.visibilityState,
2160
- activeElement: () => document.activeElement ? {
2161
- tagName: document.activeElement.tagName,
2162
- id: document.activeElement.id,
2163
- className: document.activeElement.className
2164
- } : null
2165
- },
2166
- performance: {
2167
- memory: () => window.performance && window.performance.memory ? {
2168
- used: Math.round(window.performance.memory.usedJSHeapSize / 1048576),
2169
- total: Math.round(window.performance.memory.totalJSHeapSize / 1048576),
2170
- limit: Math.round(window.performance.memory.jsHeapSizeLimit / 1048576)
2171
- } : null,
2172
- timing: () => window.performance ? {
2173
- navigationStart: window.performance.timing.navigationStart,
2174
- loadEventEnd: window.performance.timing.loadEventEnd
2175
- } : null
2176
- },
2177
- session: {
2178
- sessionId: () => this.generateSessionId(),
2179
- startTime: () => new Date().toISOString(),
2180
- pageLoadTime: () => performance.now()
2181
- }
2182
- };
2183
- }
2189
+ const Environment = {
2190
+ /**
2191
+ * Returns true if running in a browser context.
2192
+ */
2193
+ isBrowser: () => typeof window !== 'undefined' && typeof document !== 'undefined',
2184
2194
 
2185
2195
  /**
2186
- * Recolecta contexto según la configuración
2187
- * @param {Object} contextConfig - Configuración de contexto
2188
- * @returns {Object} Contexto recolectado
2196
+ * Returns the global browser object, or empty object as safe fallback.
2189
2197
  */
2190
- collect(contextConfig = {}) {
2191
- const context = {};
2198
+ getGlobal: () => (typeof window !== 'undefined' ? window : {}),
2192
2199
 
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
- });
2200
+ /**
2201
+ * Checks availability of a nested global API via dot-notation path.
2202
+ * @param {string} apiPath - e.g. 'navigator.connection' or 'crypto.randomUUID'
2203
+ * @returns {boolean}
2204
+ */
2205
+ hasApi: (apiPath) => {
2206
+ return apiPath.split('.').reduce((current, part) => {
2207
+ if (current === null || current === undefined) return null;
2208
+ return current[part] !== undefined ? current[part] : null;
2209
+ }, Environment.getGlobal()) !== null;
2210
+ },
2212
2211
 
2213
- return context;
2212
+ /**
2213
+ * Executes a task only if a condition is met; returns fallback otherwise.
2214
+ * @param {string|Function} condition - API path string or boolean-returning function.
2215
+ * @param {Function} task - Operation to execute when condition is met.
2216
+ * @param {any} fallback - Default value when condition is not met.
2217
+ * @returns {any}
2218
+ */
2219
+ runIf: (condition, task, fallback = null) => {
2220
+ const isMet = typeof condition === 'function' ? condition() : Environment.hasApi(condition);
2221
+ return isMet ? task() : fallback;
2214
2222
  }
2223
+ };
2224
+
2225
+ /**
2226
+ * Functional fragments: DOM detection and labeling utilities.
2227
+ */
2228
+ const DOM_UTILS = {
2229
+ /**
2230
+ * Selector exhaustivo de elementos interactivos.
2231
+ */
2232
+ INTERACTIVE_SELECTOR: [
2233
+ 'a', 'button', 'input', 'select', 'textarea', 'label', 'summary',
2234
+ '[role="button"]', '[role="link"]', '[role="checkbox"]', '[role="radio"]',
2235
+ '[role="menuitem"]', '[role="option"]', '[role="switch"]', '[role="tab"]',
2236
+ '.interactive', '.btn', '.clickable', // Convention selectors
2237
+ '[onclick]', '[ng-click]', '[v-on:click]', '[@click]' // Selectores de framework
2238
+ ].join(', '),
2215
2239
 
2216
2240
  /**
2217
- * Recolecta el set curado por defecto
2218
- * @param {string} contextType - Tipo de contexto
2219
- * @returns {Object} Contexto por defecto
2241
+ * Checks if an element has cursor:pointer (pure CSS fallback).
2220
2242
  */
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 {};
2243
+ hasPointerCursor: (el) => {
2244
+ try {
2245
+ return window.getComputedStyle(el).cursor === 'pointer';
2246
+ } catch (e) {
2247
+ return false;
2226
2248
  }
2249
+ },
2227
2250
 
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
- });
2251
+ /**
2252
+ * Walks up the DOM tree looking for cursor:pointer (declarative, no while).
2253
+ */
2254
+ findTargetByStyle: (el) => {
2255
+ // Guard: element nodes only
2256
+ if (!el || el.nodeType !== 1) return null;
2257
+ if (DOM_UTILS.hasPointerCursor(el)) return el;
2258
+ if (!el.parentElement || el.parentElement === document.body) return null;
2259
+ return DOM_UTILS.findTargetByStyle(el.parentElement);
2260
+ },
2237
2261
 
2238
- return result;
2239
- }
2262
+ /**
2263
+ * Finds the interactive target using closest() with CSS fallback (declarative).
2264
+ */
2265
+ findInteractiveTarget: (el) => {
2266
+ // Guard: valid element
2267
+ if (!el || el === document.body || el.nodeType !== 1) return null;
2268
+
2269
+ // Declarative search via closest()
2270
+ return el.closest(DOM_UTILS.INTERACTIVE_SELECTOR) || DOM_UTILS.findTargetByStyle(el);
2271
+ },
2240
2272
 
2241
2273
  /**
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
2274
+ * Generates a readable selector for the element.
2246
2275
  */
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
- }
2276
+ generateSelector: (el) => {
2277
+ // Guard: no element
2278
+ if (!el) return 'unknown';
2253
2279
 
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
- });
2280
+ const tag = el.tagName.toLowerCase();
2281
+ const id = el.id ? `#${el.id}` : '';
2282
+ const classes = (typeof el.className === 'string' && el.className)
2283
+ ? `.${el.className.split(' ').filter(Boolean).join('.')}`
2284
+ : '';
2267
2285
 
2268
- return result;
2286
+ return `${tag}${id}${classes}`;
2269
2287
  }
2288
+ };
2270
2289
 
2271
- /**
2272
- * Genera un ID de sesión seguro usando crypto.randomUUID() cuando esté disponible
2273
- * @returns {string} ID de sesión seguro
2274
- */
2275
- generateSecureId() {
2276
- try {
2277
- // Intentar usar crypto.randomUUID() si está disponible (Node.js 14.17+, browsers modernos)
2278
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
2279
- return crypto.randomUUID();
2280
- }
2281
-
2282
- // Fallback para navegadores más antiguos: usar crypto.getRandomValues()
2283
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
2284
- const array = new Uint8Array(16);
2285
- crypto.getRandomValues(array);
2286
-
2287
- // Convertir a formato UUID v4
2288
- const hex = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
2289
- return [
2290
- hex.slice(0, 8),
2291
- hex.slice(8, 12),
2292
- hex.slice(12, 16),
2293
- hex.slice(16, 20),
2294
- hex.slice(20, 32)
2295
- ].join('-');
2296
- }
2297
-
2298
- // Fallback final: timestamp + random (menos seguro pero funcional)
2299
- const timestamp = Date.now().toString(36);
2300
- const random = Math.random().toString(36).substring(2, 15);
2301
- return `${timestamp}-${random}`;
2302
- } catch (error) {
2303
- console.warn('SyntropyFront: Error generando ID seguro, usando fallback:', error);
2304
- // Fallback de emergencia
2305
- return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2306
- }
2290
+ /**
2291
+ * ClickInterceptor - Interceptor coordinated by functional fragments.
2292
+ * Refactored for maximum purity and removal of imperative logic.
2293
+ */
2294
+ class ClickInterceptor {
2295
+ constructor() {
2296
+ this.handler = null;
2297
+ this.lastClickTime = 0;
2298
+ this.THROTTLE_MS = 500;
2299
+ }
2300
+
2301
+ init() {
2302
+ // Guard: browser environment
2303
+ if (!Environment.isBrowser()) return;
2304
+
2305
+ this.handler = (event) => this.processClick(event);
2306
+ document.addEventListener('click', this.handler, true);
2307
2307
  }
2308
2308
 
2309
2309
  /**
2310
- * Genera un ID de sesión simple
2310
+ * Pipeline de procesamiento de clic (Pureza funcional).
2311
2311
  */
2312
- generateSessionId() {
2313
- if (!this._sessionId) {
2314
- this._sessionId = `session_${this.generateSecureId()}`;
2315
- }
2316
- return this._sessionId;
2312
+ processClick(event) {
2313
+ const el = event?.target;
2314
+
2315
+ // Guard: throttling and element existence
2316
+ if (!el || this.isThrottled()) return;
2317
+
2318
+ const target = DOM_UTILS.findInteractiveTarget(el);
2319
+ if (!target) return;
2320
+
2321
+ this.recordBreadcrumb(target);
2317
2322
  }
2318
2323
 
2319
2324
  /**
2320
- * Obtiene la lista de tipos de contexto disponibles
2321
- * @returns {Array} Tipos disponibles
2325
+ * Flow control to avoid duplicates.
2322
2326
  */
2323
- getAvailableTypes() {
2324
- return Object.keys(this.allFields);
2327
+ isThrottled() {
2328
+ const now = Date.now();
2329
+ const throttled = now - this.lastClickTime < this.THROTTLE_MS;
2330
+ if (!throttled) this.lastClickTime = now;
2331
+ return throttled;
2325
2332
  }
2326
2333
 
2327
2334
  /**
2328
- * Obtiene la lista de campos disponibles para un tipo de contexto
2329
- * @param {string} contextType - Tipo de contexto
2330
- * @returns {Array} Campos disponibles
2335
+ * Records trace in the store.
2331
2336
  */
2332
- getAvailableFields(contextType) {
2333
- const fields = this.allFields[contextType];
2334
- return fields ? Object.keys(fields) : [];
2337
+ recordBreadcrumb(target) {
2338
+ const selector = DOM_UTILS.generateSelector(target);
2339
+
2340
+ breadcrumbStore.add({
2341
+ category: 'ui',
2342
+ message: `User clicked on '${selector}'`,
2343
+ data: {
2344
+ selector,
2345
+ tagName: target.tagName,
2346
+ id: target.id || null,
2347
+ className: target.className || null,
2348
+ text: (target.innerText || target.value || '').substring(0, 30).trim()
2349
+ }
2350
+ });
2335
2351
  }
2336
2352
 
2337
2353
  /**
2338
- * Obtiene información sobre los sets por defecto
2339
- * @returns {Object} Información de sets por defecto
2354
+ * Limpieza de recursos.
2340
2355
  */
2341
- getDefaultContextsInfo() {
2342
- const info = {};
2343
- Object.entries(this.defaultContexts).forEach(([type, fields]) => {
2344
- info[type] = Object.keys(fields);
2345
- });
2346
- return info;
2356
+ destroy() {
2357
+ // Guard: browser and handler
2358
+ if (!Environment.isBrowser() || !this.handler) return;
2359
+
2360
+ document.removeEventListener('click', this.handler, true);
2361
+ this.handler = null;
2347
2362
  }
2348
2363
  }
2349
2364
 
2350
- // Instancia singleton
2351
- const contextCollector = new ContextCollector();
2352
-
2353
2365
  /**
2354
2366
  * Copyright 2024 Syntropysoft
2355
- *
2356
- * Licensed under the Apache License, Version 2.0 (the "License");
2357
- * you may not use this file except in compliance with the License.
2358
- * You may obtain a copy of the License at
2359
- *
2360
- * http://www.apache.org/licenses/LICENSE-2.0
2361
- *
2362
- * Unless required by applicable law or agreed to in writing, software
2363
- * distributed under the License is distributed on an "AS IS" BASIS,
2364
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2365
- * See the License for the specific language governing permissions and
2366
- * limitations under the License.
2367
2367
  */
2368
2368
 
2369
+ /**
2370
+ * FunctionWrapper - Utility for standardizing global handler interception.
2371
+ * Provides functional abstractions for safe execution and wrapping.
2372
+ */
2369
2373
 
2370
2374
  /**
2371
- * Interceptors - Observadores que capturan eventos automáticamente
2372
- * Implementa Chaining Pattern para coexistir con otros APMs
2375
+ * Safely executes a function with a given context and arguments.
2376
+ * @param {Function} fn - Function to execute.
2377
+ * @param {Object} context - Execution context (this).
2378
+ * @param {Array} args - Arguments to pass.
2379
+ * @param {string} [label='original handler'] - Diagnostic label for errors.
2380
+ * @returns {any} Result of the function execution or undefined if failed.
2373
2381
  */
2374
- class Interceptors {
2375
- constructor() {
2376
- this.isInitialized = false;
2377
- this.config = {
2378
- captureClicks: true,
2379
- captureFetch: true,
2380
- captureErrors: true,
2381
- captureUnhandledRejections: true
2382
- };
2383
- this.contextTypes = [];
2382
+ const safeApply = (fn, context, args, label = 'original handler') => {
2383
+ if (typeof fn !== 'function') return;
2384
+ try {
2385
+ return fn.apply(context, args);
2386
+ } catch (error) {
2387
+ // Log for instrumentation but don't crash the host application
2388
+ console.error(`SyntropyFront: Error in ${label}:`, error);
2389
+ return;
2390
+ }
2391
+ };
2384
2392
 
2385
- // Referencias originales para restaurar en destroy()
2386
- this.originalHandlers = {
2387
- fetch: null,
2388
- onerror: null,
2389
- onunhandledrejection: null
2390
- };
2393
+ /**
2394
+ * Wraps a property on a target object using a factory.
2395
+ * @param {Object} target - Target object (e.g., window).
2396
+ * @param {string} property - Property name (e.g., 'fetch').
2397
+ * @param {Function} wrapperFactory - Factory receiving (original) and returning (wrapped).
2398
+ * @returns {Object|null} Disposable object with destroy() method.
2399
+ */
2400
+ const wrap = (target, property, wrapperFactory) => {
2401
+ // `in` operator accepts null values (e.g. window.onerror === null by default)
2402
+ if (!target || !(property in target)) return null;
2403
+
2404
+ const original = target[property]; // null is a valid initial value
2405
+ const wrapped = wrapperFactory(original);
2406
+
2407
+ // Apply the wrap
2408
+ target[property] = wrapped;
2409
+
2410
+ return {
2411
+ target,
2412
+ property,
2413
+ original,
2414
+ wrapped,
2415
+ destroy: () => {
2416
+ // Only restore if it hasn't been re-wrapped by someone else (best effort)
2417
+ if (target[property] === wrapped) {
2418
+ target[property] = original;
2419
+ }
2420
+ }
2421
+ };
2422
+ };
2391
2423
 
2392
- // Event listeners para limpiar
2393
- this.eventListeners = new Map();
2394
- }
2424
+ /**
2425
+ * Copyright 2024 Syntropysoft
2426
+ */
2395
2427
 
2396
- /**
2397
- * Configura los interceptores
2398
- * @param {Object} config - Configuración de interceptores
2399
- */
2400
- configure(config) {
2401
- this.config = { ...this.config, ...config };
2402
- this.contextTypes = config.context || [];
2428
+
2429
+ /**
2430
+ * FetchInterceptor - Intercepts network calls using the wrapper (chaining) pattern.
2431
+ * Captures URL and method details to feed into the breadcrumb flow.
2432
+ */
2433
+ class FetchInterceptor {
2434
+ constructor() {
2435
+ this.wrapper = null;
2403
2436
  }
2404
2437
 
2405
2438
  /**
2406
- * Inicializa todos los interceptores
2439
+ * Initializes interception of the global fetch API.
2407
2440
  */
2408
2441
  init() {
2409
- if (this.isInitialized) {
2410
- console.warn('SyntropyFront: Interceptors ya están inicializados');
2411
- return;
2412
- }
2413
-
2414
- if (this.config.captureClicks) {
2415
- this.setupClickInterceptor();
2416
- }
2417
-
2418
- if (this.config.captureFetch) {
2419
- this.setupFetchInterceptor();
2420
- }
2421
-
2422
- if (this.config.captureErrors || this.config.captureUnhandledRejections) {
2423
- this.setupErrorInterceptors();
2424
- }
2442
+ // Guard: browser and fetch available
2443
+ if (typeof window === 'undefined' || !window.fetch) return;
2444
+
2445
+ this.wrapper = wrap(window, 'fetch', (original) => {
2446
+ return (...args) => {
2447
+ // Functional extraction of URL and method
2448
+ const url = (args[0] instanceof Request) ? args[0].url : (args[0] || 'unknown');
2449
+ const method = (args[0] instanceof Request) ? args[0].method : (args[1]?.method || 'GET');
2450
+
2451
+ breadcrumbStore.add({
2452
+ category: 'network',
2453
+ message: `Request: ${method} ${url}`,
2454
+ data: {
2455
+ url,
2456
+ method,
2457
+ timestamp: Date.now()
2458
+ }
2459
+ });
2425
2460
 
2426
- this.isInitialized = true;
2427
- console.log('SyntropyFront: Interceptors inicializados con Chaining Pattern');
2461
+ // Continue with original execution
2462
+ return original.apply(window, args);
2463
+ };
2464
+ });
2428
2465
  }
2429
2466
 
2430
2467
  /**
2431
- * Intercepta clics de usuario
2468
+ * Restores original fetch behaviour.
2432
2469
  */
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;
2470
+ destroy() {
2471
+ // Guard: no wrapper
2472
+ if (!this.wrapper) return;
2442
2473
 
2443
- const clickHandler = (event) => {
2444
- const el = event.target;
2445
- if (!el) return;
2474
+ this.wrapper.destroy();
2475
+ this.wrapper = null;
2476
+ }
2477
+ }
2446
2478
 
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;
2479
+ /**
2480
+ * Pure function: Generates a secure ID using an injected crypto provider.
2481
+ * Accepts a cryptoApi object so all fallback branches are independently testable.
2482
+ * @param {Object|null} cryptoApi - Crypto implementation (e.g. window.crypto or null)
2483
+ * @returns {string} Secure random ID
2484
+ */
2485
+ const createSecureId = (cryptoApi = (typeof crypto !== 'undefined' ? crypto : null)) => {
2486
+ if (typeof cryptoApi?.randomUUID === 'function') {
2487
+ return cryptoApi.randomUUID();
2488
+ }
2489
+ if (typeof cryptoApi?.getRandomValues === 'function') {
2490
+ const array = new Uint8Array(16);
2491
+ cryptoApi.getRandomValues(array);
2492
+ return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
2493
+ }
2494
+ return `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2495
+ };
2451
2496
 
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'));
2497
+ /**
2498
+ * Functional fragments: pure context providers.
2499
+ * Uses Environment.runIf for safe access.
2500
+ */
2501
+ const CONTEXT_PROVIDERS = {
2502
+ device: {
2503
+ userAgent: () => Environment.runIf('navigator.userAgent', () => navigator.userAgent),
2504
+ language: () => Environment.runIf('navigator.language', () => navigator.language),
2505
+ languages: () => Environment.runIf('navigator.languages', () => navigator.languages),
2506
+ screen: () => Environment.runIf('window.screen', () => ({
2507
+ width: window.screen.width,
2508
+ height: window.screen.height,
2509
+ availWidth: window.screen.availWidth,
2510
+ availHeight: window.screen.availHeight,
2511
+ colorDepth: window.screen.colorDepth,
2512
+ pixelDepth: window.screen.pixelDepth
2513
+ })),
2514
+ timezone: () => Environment.runIf('Intl.DateTimeFormat', () => {
2515
+ try {
2516
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
2517
+ } catch (e) { return null; }
2518
+ }),
2519
+ cookieEnabled: () => Environment.runIf('navigator.cookieEnabled', () => navigator.cookieEnabled),
2520
+ doNotTrack: () => Environment.runIf('navigator.doNotTrack', () => navigator.doNotTrack)
2521
+ },
2522
+ window: {
2523
+ url: () => Environment.runIf('window.location.href', () => window.location.href),
2524
+ pathname: () => Environment.runIf('window.location.pathname', () => window.location.pathname),
2525
+ search: () => Environment.runIf('window.location.search', () => window.location.search),
2526
+ hash: () => Environment.runIf('window.location.hash', () => window.location.hash),
2527
+ referrer: () => Environment.runIf('document.referrer', () => document.referrer),
2528
+ title: () => Environment.runIf('document.title', () => document.title),
2529
+ viewport: () => Environment.runIf('window.innerWidth', () => ({
2530
+ width: window.innerWidth,
2531
+ height: window.innerHeight
2532
+ }))
2533
+ },
2534
+ storage: {
2535
+ localStorage: () => Environment.runIf('localStorage', () => {
2536
+ try {
2537
+ const storage = window.localStorage;
2538
+ return {
2539
+ keys: Object.keys(storage).length,
2540
+ size: JSON.stringify(storage).length,
2541
+ keyNames: Object.keys(storage)
2542
+ };
2543
+ } catch (e) { return null; }
2544
+ }),
2545
+ sessionStorage: () => Environment.runIf('sessionStorage', () => {
2546
+ try {
2547
+ const storage = window.sessionStorage;
2548
+ return {
2549
+ keys: Object.keys(storage).length,
2550
+ size: JSON.stringify(storage).length,
2551
+ keyNames: Object.keys(storage)
2552
+ };
2553
+ } catch (e) { return null; }
2554
+ })
2555
+ },
2556
+ network: {
2557
+ online: () => Environment.runIf('navigator.onLine', () => navigator.onLine),
2558
+ connection: () => Environment.runIf(() => !!(typeof navigator !== 'undefined' && (navigator.connection || navigator.mozConnection || navigator.webkitConnection)), () => {
2559
+ const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
2560
+ return {
2561
+ effectiveType: conn.effectiveType,
2562
+ downlink: conn.downlink,
2563
+ rtt: conn.rtt
2564
+ };
2565
+ })
2566
+ },
2567
+ ui: {
2568
+ focused: () => Environment.runIf('document.hasFocus', () => document.hasFocus()),
2569
+ visibility: () => Environment.runIf('document.visibilityState', () => document.visibilityState),
2570
+ activeElement: () => Environment.runIf('document.activeElement', () => ({
2571
+ tagName: document.activeElement.tagName,
2572
+ id: document.activeElement.id,
2573
+ className: document.activeElement.className
2574
+ }))
2575
+ },
2576
+ performance: {
2577
+ memory: () => Environment.runIf('performance.memory', () => ({
2578
+ used: Math.round(performance.memory.usedJSHeapSize / 1048576),
2579
+ total: Math.round(performance.memory.totalJSHeapSize / 1048576),
2580
+ limit: Math.round(performance.memory.jsHeapSizeLimit / 1048576)
2581
+ })),
2582
+ timing: () => Environment.runIf('performance.timing', () => ({
2583
+ navigationStart: performance.timing.navigationStart,
2584
+ loadEventEnd: performance.timing.loadEventEnd
2585
+ }))
2586
+ },
2587
+ session: {
2588
+ sessionId: (collector) => collector.generateSessionId(),
2589
+ startTime: () => new Date().toISOString(),
2590
+ pageLoadTime: () => Environment.runIf('performance.now', () => performance.now())
2591
+ }
2592
+ };
2457
2593
 
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
- }
2594
+ /**
2595
+ * Default fields for context collection.
2596
+ */
2597
+ const DEFAULT_CONTEXT_FIELDS = {
2598
+ device: ['userAgent', 'language', 'screen', 'timezone'],
2599
+ window: ['url', 'viewport', 'title'],
2600
+ session: ['sessionId', 'pageLoadTime'],
2601
+ ui: ['visibility', 'activeElement'],
2602
+ network: ['online', 'connection']
2603
+ };
2464
2604
 
2465
- return interactiveTags.includes(element.tagName.toLowerCase()) || isClickableRole || hasPointerCursor;
2466
- };
2605
+ /**
2606
+ * ContextCollector - Minimal coordinator with no imperative state.
2607
+ */
2608
+ class ContextCollector {
2609
+ constructor() {
2610
+ this.sessionId = null;
2611
+ this.providers = new Map(Object.entries(CONTEXT_PROVIDERS));
2612
+ }
2467
2613
 
2468
- // Si el elemento no es interactivo, buscar hacia arriba en el DOM (bubbling)
2469
- let target = el;
2470
- while (target && target !== document.body) {
2471
- if (isInteractive(target)) break;
2472
- target = target.parentElement;
2473
- }
2614
+ /**
2615
+ * Collects context based on a declarative configuration.
2616
+ */
2617
+ collect(contextConfig = {}) {
2618
+ const config = this.normalizeConfig(contextConfig);
2474
2619
 
2475
- // Si no encontramos un elemento interactivo, ignoramos el click (reduce ruido)
2476
- if (!target || target === document.body) return;
2620
+ return Object.entries(config).reduce((ctx, [type, options]) => {
2621
+ try {
2622
+ const provider = this.providers.get(type);
2623
+ if (options !== false && !provider) throw new Error(`Provider for '${type}' not found`);
2477
2624
 
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('.')}`;
2625
+ const fields = this.resolveFields(type, provider, options);
2626
+ if (fields) {
2627
+ ctx[type] = this.extractFields(provider, fields);
2628
+ }
2629
+ } catch (error) {
2630
+ console.warn(`SyntropyFront: Error collecting context for ${type}:`, error);
2631
+ ctx[type] = { error: 'Collection failed' };
2484
2632
  }
2633
+ return ctx;
2634
+ }, {});
2635
+ }
2485
2636
 
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
- });
2637
+ /**
2638
+ * Field resolution — declarative strategy map by type.
2639
+ * Contracts:
2640
+ * boolean true → default fields for type, or all provider keys
2641
+ * boolean false → null (skip)
2642
+ * array → explicit field list
2643
+ * plain object → all provider keys (treated as unstructured custom config)
2644
+ * other → null (skip)
2645
+ */
2646
+ resolveFields(type, provider, options) {
2647
+ const allProviderKeys = () => Object.keys(provider || {});
2648
+ const strategies = {
2649
+ boolean: (val) => val ? (DEFAULT_CONTEXT_FIELDS[type] || allProviderKeys()) : null,
2650
+ object: (val) => Array.isArray(val) ? val : allProviderKeys(),
2497
2651
  };
2498
2652
 
2499
- // Guardar referencia para limpiar después
2500
- this.eventListeners.set('click', clickHandler);
2501
- document.addEventListener('click', clickHandler, true);
2653
+ const strategy = strategies[typeof options];
2654
+ return strategy ? strategy(options) : null;
2502
2655
  }
2503
2656
 
2504
2657
  /**
2505
- * Intercepta llamadas de red (fetch) con Chaining
2506
- */
2507
- setupFetchInterceptor() {
2508
- // Solo configurar en el browser
2509
- if (typeof window === 'undefined' || !window.fetch) {
2510
- console.log('SyntropyFront: Fetch interceptor no disponible (no browser/fetch)');
2511
- return;
2658
+ * Functional normalization of configuration.
2659
+ */
2660
+ normalizeConfig(config) {
2661
+ if (Array.isArray(config)) {
2662
+ return config.reduce((acc, type) => ({ ...acc, [type]: true }), {});
2512
2663
  }
2664
+ return config || {};
2665
+ }
2513
2666
 
2514
- // Guardar referencia original
2515
- this.originalHandlers.fetch = window.fetch;
2667
+ /**
2668
+ * Pure field extraction.
2669
+ */
2670
+ extractFields(provider, fieldNames) {
2671
+ return fieldNames.reduce((result, fieldName) => {
2672
+ const getter = provider[fieldName];
2673
+ if (typeof getter !== 'function') return result;
2516
2674
 
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');
2675
+ try {
2676
+ result[fieldName] = getter(this);
2677
+ } catch (e) {
2678
+ console.warn(`SyntropyFront: Error collecting field ${fieldName}:`, e);
2679
+ result[fieldName] = null;
2680
+ }
2681
+ return result;
2682
+ }, {});
2683
+ }
2521
2684
 
2522
- breadcrumbStore.add({
2523
- category: 'network',
2524
- message: `Request: ${method} ${url}`,
2525
- data: {
2526
- url,
2527
- method,
2528
- timestamp: Date.now()
2529
- }
2530
- });
2685
+ registerProvider(name, fields) {
2686
+ this.providers.set(name, fields);
2687
+ }
2531
2688
 
2532
- // ✅ CHAINING: Llamar al handler original
2533
- return this.originalHandlers.fetch.apply(window, args);
2534
- };
2689
+ generateSessionId() {
2690
+ this.sessionId = this.sessionId || `session_${this.generateSecureId()}`;
2691
+ return this.sessionId;
2692
+ }
2535
2693
 
2536
- // Sobrescribir con el nuevo handler
2537
- window.fetch = syntropyFetchHandler;
2694
+ // Delegates to the pure top-level createSecureId (injectable for testing)
2695
+ generateSecureId(cryptoApi = (typeof crypto !== 'undefined' ? crypto : null)) {
2696
+ return createSecureId(cryptoApi);
2538
2697
  }
2539
2698
 
2540
- /**
2541
- * Intercepta errores globales con Chaining
2542
- */
2543
- setupErrorInterceptors() {
2544
- // Solo configurar en el browser
2545
- if (typeof window === 'undefined') {
2546
- console.log('SyntropyFront: Error interceptors no disponibles (no browser)');
2547
- return;
2548
- }
2699
+ getAvailableTypes() {
2700
+ return Array.from(this.providers.keys());
2701
+ }
2549
2702
 
2550
- if (this.config.captureErrors) {
2551
- // Guardar referencia original
2552
- this.originalHandlers.onerror = window.onerror;
2553
-
2554
- // Crear nuevo handler que encadena con el original
2555
- const syntropyErrorHandler = (message, source, lineno, colno, error) => {
2556
- const errorPayload = {
2557
- type: 'uncaught_exception',
2558
- error: {
2559
- message,
2560
- source,
2561
- lineno,
2562
- colno,
2563
- stack: error?.stack
2564
- },
2565
- breadcrumbs: breadcrumbStore.getAll(),
2566
- timestamp: new Date().toISOString()
2567
- };
2703
+ get allFields() {
2704
+ return Array.from(this.providers.entries()).reduce((res, [name, fields]) => {
2705
+ res[name] = fields;
2706
+ return res;
2707
+ }, {});
2708
+ }
2568
2709
 
2569
- this.handleError(errorPayload);
2710
+ get defaultContexts() {
2711
+ return this.allFields;
2712
+ }
2713
+ }
2570
2714
 
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
- }
2715
+ const contextCollector = new ContextCollector();
2580
2716
 
2581
- return false; // No prevenir el error por defecto
2582
- };
2717
+ /**
2718
+ * Functional fragments: pure error payload generators.
2719
+ */
2720
+ const ERROR_UTILS = {
2721
+ createExceptionPayload: (message, source, lineno, colno, error) => ({
2722
+ type: 'uncaught_exception',
2723
+ error: { message, source, lineno, colno, stack: error?.stack },
2724
+ breadcrumbs: breadcrumbStore.getAll(),
2725
+ timestamp: new Date().toISOString()
2726
+ }),
2727
+
2728
+ createRejectionPayload: (event) => ({
2729
+ type: 'unhandled_rejection',
2730
+ error: {
2731
+ message: event.reason?.message || 'Promise rejection without message',
2732
+ stack: event.reason?.stack,
2733
+ },
2734
+ breadcrumbs: breadcrumbStore.getAll(),
2735
+ timestamp: new Date().toISOString()
2736
+ })
2737
+ };
2583
2738
 
2584
- // Sobrescribir con el nuevo handler
2585
- window.onerror = syntropyErrorHandler;
2586
- }
2739
+ /**
2740
+ * ErrorInterceptor - Error capture coordinated by functional fragments.
2741
+ * Refactored to remove heavy imperative logic and use FunctionWrapper consistently.
2742
+ */
2743
+ class ErrorInterceptor {
2744
+ constructor() {
2745
+ this.errorWrapper = null;
2746
+ this.rejectionWrapper = null;
2747
+ this.config = { captureErrors: true, captureUnhandledRejections: true };
2748
+ this.contextTypes = [];
2749
+ this.onErrorCallback = null;
2750
+ }
2587
2751
 
2588
- if (this.config.captureUnhandledRejections) {
2589
- // Guardar referencia original
2590
- this.originalHandlers.onunhandledrejection = window.onunhandledrejection;
2752
+ /**
2753
+ * Dynamic configuration of the interceptor.
2754
+ */
2755
+ configure(config, contextTypes, onErrorCallback) {
2756
+ this.config = { ...this.config, ...config };
2757
+ this.contextTypes = contextTypes || [];
2758
+ this.onErrorCallback = onErrorCallback;
2759
+ }
2591
2760
 
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
- };
2761
+ /**
2762
+ * Selective initialization.
2763
+ */
2764
+ init() {
2765
+ // Guard: browser environment
2766
+ if (!Environment.isBrowser()) return;
2603
2767
 
2604
- this.handleError(errorPayload);
2768
+ this.setupExceptionCapture();
2769
+ this.setupRejectionCapture();
2770
+ }
2605
2771
 
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
- };
2772
+ /**
2773
+ * Sets up the wrapper for window.onerror (exceptions).
2774
+ */
2775
+ setupExceptionCapture() {
2776
+ // Guard: capture disabled
2777
+ if (!this.config.captureErrors) return;
2615
2778
 
2616
- // Sobrescribir con el nuevo handler
2617
- window.onunhandledrejection = syntropyRejectionHandler;
2618
- }
2779
+ this.errorWrapper = wrap(window, 'onerror', (original) => {
2780
+ return (message, source, lineno, colno, error) => {
2781
+ const payload = ERROR_UTILS.createExceptionPayload(message, source, lineno, colno, error);
2782
+ this.handleError(payload);
2783
+ return safeApply(original, window, [message, source, lineno, colno, error], 'original window.onerror');
2784
+ };
2785
+ });
2619
2786
  }
2620
2787
 
2621
2788
  /**
2622
- * Maneja los errores capturados
2623
- * @param {Object} errorPayload - Payload del error
2789
+ * Sets up the wrapper for promise rejections.
2624
2790
  */
2625
- handleError(errorPayload) {
2626
- // Recolectar contexto si está configurado
2627
- const context = this.contextTypes.length > 0 ? contextCollector.collect(this.contextTypes) : null;
2791
+ setupRejectionCapture() {
2792
+ // Guard: capture disabled
2793
+ if (!this.config.captureUnhandledRejections) return;
2628
2794
 
2629
- // Enviar al agent si está configurado
2630
- agent.sendError(errorPayload, context);
2795
+ this.rejectionWrapper = wrap(window, 'onunhandledrejection', (original) => {
2796
+ return (event) => {
2797
+ const payload = ERROR_UTILS.createRejectionPayload(event);
2798
+ this.handleError(payload);
2799
+ safeApply(original, window, [event], 'original window.onunhandledrejection');
2800
+ };
2801
+ });
2802
+ }
2631
2803
 
2632
- // Callback para manejo personalizado de errores
2633
- if (this.onError) {
2634
- this.onError(errorPayload);
2635
- } else {
2636
- // Comportamiento por defecto: log a consola
2637
- console.error('SyntropyFront - Error detectado:', errorPayload);
2638
- }
2804
+ /**
2805
+ * Functional delegation for error handling and send.
2806
+ */
2807
+ handleError(payload) {
2808
+ const context = this.contextTypes.length > 0 ? contextCollector.collect(this.contextTypes) : null;
2809
+ agent.sendError(payload, context);
2810
+ if (this.onErrorCallback) this.onErrorCallback(payload);
2639
2811
  }
2640
2812
 
2641
2813
  /**
2642
- * Desactiva todos los interceptores y restaura handlers originales
2814
+ * Cleans up wrappers to avoid memory leaks.
2643
2815
  */
2644
2816
  destroy() {
2645
- if (!this.isInitialized) return;
2817
+ this.errorWrapper?.destroy();
2818
+ this.rejectionWrapper?.destroy();
2819
+ this.errorWrapper = null;
2820
+ this.rejectionWrapper = null;
2821
+ }
2822
+ }
2646
2823
 
2647
- console.log('SyntropyFront: Limpiando interceptores...');
2824
+ /**
2825
+ * Copyright 2024 Syntropysoft
2826
+ */
2648
2827
 
2649
- // ✅ RESTAURAR: Handlers originales
2650
- if (this.originalHandlers.fetch) {
2651
- window.fetch = this.originalHandlers.fetch;
2652
- console.log('SyntropyFront: fetch original restaurado');
2653
- }
2654
2828
 
2655
- if (this.originalHandlers.onerror) {
2656
- window.onerror = this.originalHandlers.onerror;
2657
- console.log('SyntropyFront: onerror original restaurado');
2658
- }
2829
+ /**
2830
+ * Interceptors - Coordinator for modular interceptors.
2831
+ * Follows SOLID principles by treating interceptors as an extensible collection.
2832
+ * Registry pattern architecture.
2833
+ */
2834
+ class Interceptors {
2835
+ constructor() {
2836
+ this.isInitialized = false;
2837
+ this.config = {
2838
+ captureClicks: true,
2839
+ captureFetch: true,
2840
+ captureErrors: true,
2841
+ captureUnhandledRejections: true
2842
+ };
2659
2843
 
2660
- if (this.originalHandlers.onunhandledrejection) {
2661
- window.onunhandledrejection = this.originalHandlers.onunhandledrejection;
2662
- console.log('SyntropyFront: onunhandledrejection original restaurado');
2663
- }
2844
+ this.registry = new Map();
2845
+ this.initializeRegistry();
2846
+ }
2664
2847
 
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
- }
2848
+ /**
2849
+ * Declarative registration of standard interceptors.
2850
+ */
2851
+ initializeRegistry() {
2852
+ this.registry.set('clicks', new ClickInterceptor());
2853
+ this.registry.set('fetch', new FetchInterceptor());
2854
+ this.registry.set('errors', new ErrorInterceptor());
2855
+ }
2672
2856
 
2673
- // Limpiar referencias
2674
- this.originalHandlers = {
2675
- fetch: null,
2676
- onerror: null,
2677
- onunhandledrejection: null
2678
- };
2679
- this.eventListeners.clear();
2680
- this.isInitialized = false;
2857
+ /**
2858
+ * Decoupled configuration.
2859
+ */
2860
+ configure(config = {}) {
2861
+ this.config = { ...this.config, ...config };
2862
+ // Inject config and optional callbacks into interceptors that need them
2863
+ this.registry.get('errors')?.configure?.(this.config, config.context || [], config.onError);
2864
+ }
2865
+
2866
+ /**
2867
+ * Initialization via functional pipeline.
2868
+ */
2869
+ init() {
2870
+ // Guard: already initialized
2871
+ if (this.isInitialized) return;
2681
2872
 
2682
- console.log('SyntropyFront: Interceptors destruidos y handlers restaurados');
2873
+ this.runLifecycle('init');
2874
+
2875
+ this.isInitialized = true;
2876
+ console.log('SyntropyFront: Interceptors initialized (Refactored architecture)');
2683
2877
  }
2684
2878
 
2685
2879
  /**
2686
- * Obtiene información sobre los handlers originales
2687
- * @returns {Object} Información de handlers
2688
- */
2689
- getHandlerInfo() {
2690
- return {
2691
- isInitialized: this.isInitialized,
2692
- hasOriginalFetch: !!this.originalHandlers.fetch,
2693
- hasOriginalOnError: !!this.originalHandlers.onerror,
2694
- hasOriginalOnUnhandledRejection: !!this.originalHandlers.onunhandledrejection,
2695
- eventListenersCount: this.eventListeners.size
2696
- };
2880
+ * Teardown via functional pipeline.
2881
+ */
2882
+ destroy() {
2883
+ // Guard: not initialized
2884
+ if (!this.isInitialized) return;
2885
+
2886
+ this.runLifecycle('destroy');
2887
+
2888
+ this.isInitialized = false;
2889
+ console.log('SyntropyFront: Interceptors destroyed');
2697
2890
  }
2891
+
2892
+ /**
2893
+ * Runs a lifecycle method across the registry based on config.
2894
+ * Functional pipeline: filter -> map -> filter -> forEach.
2895
+ */
2896
+ runLifecycle(method) {
2897
+ const lifecycleMap = [
2898
+ { key: 'clicks', enabled: this.config.captureClicks },
2899
+ { key: 'fetch', enabled: this.config.captureFetch },
2900
+ { key: 'errors', enabled: this.config.captureErrors || this.config.captureUnhandledRejections }
2901
+ ];
2902
+
2903
+ lifecycleMap
2904
+ .filter(item => item.enabled)
2905
+ .map(item => this.registry.get(item.key))
2906
+ .filter(interceptor => interceptor && typeof interceptor[method] === 'function')
2907
+ .forEach(interceptor => interceptor[method]());
2908
+ }
2909
+
2910
+ // Compatibility accessors for tests or controlled direct access
2911
+ get clickInterceptor() { return this.registry.get('clicks'); }
2912
+ get fetchInterceptor() { return this.registry.get('fetch'); }
2913
+ get errorInterceptor() { return this.registry.get('errors'); }
2698
2914
  }
2699
2915
 
2700
- // Instancia singleton
2701
2916
  const interceptors = new Interceptors();
2702
2917
 
2703
2918
  /**
@@ -2729,109 +2944,101 @@ class SyntropyFront {
2729
2944
  captureFetch: true,
2730
2945
  captureErrors: true,
2731
2946
  captureUnhandledRejections: true,
2947
+ samplingRate: 1.0,
2732
2948
  onError: null
2733
2949
  };
2734
2950
 
2735
- // Auto-inicializar
2951
+ // Auto-initialize
2736
2952
  this.init();
2737
2953
  }
2738
2954
 
2739
2955
  /**
2740
- * Inicializa la biblioteca y activa los interceptores
2956
+ * Initializes the library and activates interceptors
2741
2957
  */
2742
2958
  init() {
2743
2959
  if (this.isActive) return;
2744
2960
 
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
-
2961
+ this._applyConfig();
2765
2962
  interceptors.init();
2766
2963
 
2767
- // Intentar reintentar items fallidos de sesiones previas
2964
+ // Retry failed items from previous sessions
2768
2965
  agent.retryFailedItems().catch(err => {
2769
- console.warn('SyntropyFront: Error al intentar recuperar items persistentes:', err);
2966
+ console.warn('SyntropyFront: Error attempting to recover persistent items:', err);
2770
2967
  });
2771
2968
 
2772
2969
  this.isActive = true;
2773
- console.log('🚀 SyntropyFront: Inicializado con arquitectura modular resiliente');
2970
+ console.log('🚀 SyntropyFront: Initialized with modular resilient architecture');
2774
2971
  }
2775
2972
 
2776
2973
  /**
2777
- * Configura SyntropyFront
2778
- * @param {Object} config - Configuración
2974
+ * Private: applies current config to agent and interceptors.
2779
2975
  */
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
2976
+ _applyConfig() {
2791
2977
  agent.configure({
2792
2978
  endpoint: this.config.endpoint,
2793
2979
  headers: this.config.headers,
2794
- usePersistentBuffer: this.config.usePersistentBuffer
2980
+ usePersistentBuffer: this.config.usePersistentBuffer,
2981
+ samplingRate: this.config.samplingRate,
2982
+ batchTimeout: this.config.batchTimeout ?? (this.config.captureClicks || this.config.captureFetch ? 5000 : null)
2795
2983
  });
2796
2984
 
2985
+ breadcrumbStore.onBreadcrumbAdded = (crumb) => {
2986
+ if (agent.isEnabled() && agent.shouldSendBreadcrumbs()) {
2987
+ agent.sendBreadcrumbs([crumb]);
2988
+ }
2989
+ };
2990
+
2797
2991
  interceptors.configure({
2798
2992
  captureClicks: this.config.captureClicks,
2799
2993
  captureFetch: this.config.captureFetch,
2800
2994
  captureErrors: this.config.captureErrors,
2801
- captureUnhandledRejections: this.config.captureUnhandledRejections
2995
+ captureUnhandledRejections: this.config.captureUnhandledRejections,
2996
+ onError: this.config.onError
2802
2997
  });
2998
+ }
2803
2999
 
2804
- if (this.config.onError) {
2805
- interceptors.onError = this.config.onError;
3000
+ /**
3001
+ * Configures SyntropyFront
3002
+ * @param {Object} config - Configuration
3003
+ */
3004
+ configure(config = {}) {
3005
+ this.config = { ...this.config, ...config };
3006
+
3007
+ // If 'fetch' is passed, extract endpoint and headers for compatibility
3008
+ if (config.fetch) {
3009
+ this.config.endpoint = config.fetch.url;
3010
+ this.config.headers = config.fetch.options?.headers || {};
2806
3011
  }
2807
3012
 
3013
+ this._applyConfig();
3014
+
2808
3015
  const mode = this.config.endpoint ? `endpoint: ${this.config.endpoint}` : 'console only';
2809
- console.log(`✅ SyntropyFront: Configurado - ${mode}`);
3016
+ console.log(`✅ SyntropyFront: Configured - ${mode}`);
2810
3017
  }
2811
3018
 
2812
3019
  /**
2813
- * Añade un breadcrumb manualmente
3020
+ * Adds a breadcrumb manually
2814
3021
  */
2815
3022
  addBreadcrumb(category, message, data = {}) {
2816
3023
  return breadcrumbStore.add({ category, message, data });
2817
3024
  }
2818
3025
 
2819
3026
  /**
2820
- * Obtiene todos los breadcrumbs
3027
+ * Gets all breadcrumbs
2821
3028
  */
2822
3029
  getBreadcrumbs() {
2823
3030
  return breadcrumbStore.getAll();
2824
3031
  }
2825
3032
 
2826
3033
  /**
2827
- * Limpia los breadcrumbs
3034
+ * Clears breadcrumbs
2828
3035
  */
2829
3036
  clearBreadcrumbs() {
2830
3037
  breadcrumbStore.clear();
2831
3038
  }
2832
3039
 
2833
3040
  /**
2834
- * Envía un error manualmente con contexto
3041
+ * Sends an error manually with context
2835
3042
  */
2836
3043
  sendError(error, context = {}) {
2837
3044
  const errorPayload = {
@@ -2850,14 +3057,14 @@ class SyntropyFront {
2850
3057
  }
2851
3058
 
2852
3059
  /**
2853
- * Fuerza el envío de datos pendientes
3060
+ * Forces sending pending data
2854
3061
  */
2855
3062
  async flush() {
2856
3063
  await agent.forceFlush();
2857
3064
  }
2858
3065
 
2859
3066
  /**
2860
- * Obtiene estadísticas de uso
3067
+ * Gets usage statistics
2861
3068
  */
2862
3069
  getStats() {
2863
3070
  return {
@@ -2869,17 +3076,17 @@ class SyntropyFront {
2869
3076
  }
2870
3077
 
2871
3078
  /**
2872
- * Desactiva la biblioteca y restaura hooks originales
3079
+ * Deactivates the library and restores original hooks
2873
3080
  */
2874
3081
  destroy() {
2875
3082
  interceptors.destroy();
2876
3083
  agent.disable();
2877
3084
  this.isActive = false;
2878
- console.log('SyntropyFront: Desactivado');
3085
+ console.log('SyntropyFront: Deactivated');
2879
3086
  }
2880
3087
  }
2881
3088
 
2882
- // Instancia única (Singleton)
3089
+ // Singleton instance
2883
3090
  const syntropyFront = new SyntropyFront();
2884
3091
 
2885
3092
  export { syntropyFront as default };