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