@syntropysoft/syntropyfront 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODE_OF_CONDUCT.md +46 -0
- package/CONTRIBUTING.md +73 -0
- package/NOTICE +2 -0
- package/README.md +178 -467
- package/SECURITY.md +25 -0
- package/dist/index.cjs +1563 -1356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +1563 -1356
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -3,51 +3,45 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* BreadcrumbStore -
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
18
|
-
* @param {
|
|
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
|
-
//
|
|
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
|
-
*
|
|
39
|
-
* @returns {number}
|
|
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
|
-
*
|
|
47
|
-
* @param {Object} crumb -
|
|
48
|
-
* @param {string} crumb.category -
|
|
49
|
-
* @param {string} crumb.message -
|
|
50
|
-
* @param {Object} [crumb.data] -
|
|
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
|
-
|
|
59
|
-
|
|
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.
|
|
57
|
+
this.onBreadcrumbAdded(breadcrumb);
|
|
73
58
|
} catch (error) {
|
|
74
|
-
console.warn('SyntropyFront: Error
|
|
59
|
+
console.warn('SyntropyFront: Error in onBreadcrumbAdded:', error);
|
|
75
60
|
}
|
|
76
61
|
}
|
|
77
62
|
}
|
|
78
63
|
|
|
79
64
|
/**
|
|
80
|
-
*
|
|
81
|
-
* @returns {Array}
|
|
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
|
-
*
|
|
73
|
+
* Clears all breadcrumbs
|
|
89
74
|
*/
|
|
90
75
|
clear() {
|
|
91
76
|
this.breadcrumbs = [];
|
|
92
77
|
}
|
|
93
78
|
|
|
94
79
|
/**
|
|
95
|
-
*
|
|
96
|
-
* @
|
|
97
|
-
|
|
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
|
-
//
|
|
97
|
+
// Singleton instance
|
|
105
98
|
const breadcrumbStore = new BreadcrumbStore();
|
|
106
99
|
|
|
107
100
|
/**
|
|
108
|
-
* ConfigurationManager -
|
|
109
|
-
*
|
|
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
|
-
*
|
|
130
|
-
* @param {Object} config -
|
|
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
|
-
|
|
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
|
-
*
|
|
142
|
+
* Checks if the agent is enabled
|
|
148
143
|
*/
|
|
149
144
|
isAgentEnabled() {
|
|
150
145
|
return this.isEnabled;
|
|
151
146
|
}
|
|
152
147
|
|
|
153
148
|
/**
|
|
154
|
-
*
|
|
149
|
+
* Checks if it should send breadcrumbs
|
|
155
150
|
*/
|
|
156
151
|
shouldSendBreadcrumbs() {
|
|
157
152
|
return this.sendBreadcrumbs;
|
|
158
153
|
}
|
|
159
154
|
|
|
160
155
|
/**
|
|
161
|
-
*
|
|
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 -
|
|
182
|
-
*
|
|
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;
|
|
186
|
+
this.flushCallback = null;
|
|
190
187
|
}
|
|
191
188
|
|
|
192
189
|
/**
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
251
|
-
|
|
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 -
|
|
268
|
-
*
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
330
|
+
this.processRetryQueue(this.sendCallback, this.removePersistentCallback);
|
|
331
|
+
}, delay);
|
|
308
332
|
}
|
|
309
333
|
|
|
310
334
|
/**
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
394
|
+
* Gets the retry queue size
|
|
380
395
|
*/
|
|
381
396
|
getSize() {
|
|
382
397
|
return this.retryQueue.length;
|
|
383
398
|
}
|
|
384
399
|
|
|
385
400
|
/**
|
|
386
|
-
*
|
|
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
|
-
*
|
|
395
|
-
*
|
|
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
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
493
|
-
|
|
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
|
-
|
|
498
|
-
if (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
554
|
-
if (
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
573
|
-
* @param {string} jsonString - JSON string
|
|
574
|
-
* @returns {any}
|
|
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
|
|
567
|
+
console.error('SyntropyFront: Error in deserialization:', error);
|
|
582
568
|
return null;
|
|
583
569
|
}
|
|
584
570
|
}
|
|
585
571
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
*
|
|
680
|
-
* @param {any} obj -
|
|
681
|
-
* @returns {string} JSON string
|
|
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
|
|
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
|
-
//
|
|
637
|
+
// Singleton instance
|
|
698
638
|
const robustSerializer = new RobustSerializer();
|
|
699
639
|
|
|
700
640
|
/**
|
|
701
|
-
* HttpTransport -
|
|
702
|
-
*
|
|
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
|
-
|
|
711
|
-
|
|
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
|
-
// ✅
|
|
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
|
|
725
|
-
|
|
726
|
-
// Fallback:
|
|
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: '
|
|
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
|
-
|
|
751
|
-
|
|
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 -
|
|
770
|
-
*
|
|
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
|
-
|
|
781
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
810
|
-
|
|
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 = '
|
|
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
|
|
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
|
-
|
|
835
|
-
|
|
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
|
-
|
|
847
|
-
|
|
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 -
|
|
859
|
-
*
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
811
|
+
* Initializes the connection to IndexedDB
|
|
812
|
+
* @returns {Promise<Object>} Init result
|
|
813
|
+
*/
|
|
872
814
|
async init() {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
834
|
+
try {
|
|
895
835
|
const connectionResult = await this.openConnection();
|
|
896
836
|
if (!connectionResult.success) {
|
|
897
|
-
|
|
898
|
-
|
|
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
|
|
847
|
+
return { success: true, error: null, timestamp: new Date().toISOString() };
|
|
906
848
|
} catch (error) {
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
914
|
-
|
|
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
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
*/
|
|
893
|
+
* Closes the database connection
|
|
894
|
+
*/
|
|
952
895
|
close() {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
error:
|
|
956
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
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
|
-
|
|
977
|
-
|
|
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
|
-
|
|
985
|
-
|
|
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 -
|
|
994
|
-
*
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
|
998
|
+
operationResult.error = `Error in read operation: ${error.message}`;
|
|
1065
999
|
return operationResult;
|
|
1066
1000
|
}
|
|
1067
1001
|
}
|
|
1068
1002
|
|
|
1069
1003
|
/**
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
|
1027
|
+
operationResult.error = `Error in write operation: ${error.message}`;
|
|
1094
1028
|
return operationResult;
|
|
1095
1029
|
}
|
|
1096
1030
|
}
|
|
1097
1031
|
|
|
1098
1032
|
/**
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
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 -
|
|
1123
|
-
*
|
|
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
|
-
|
|
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.
|
|
1140
|
-
|
|
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
|
-
|
|
1076
|
+
|
|
1077
|
+
console.log('SyntropyFront: Database initialized');
|
|
1078
|
+
return true;
|
|
1145
1079
|
}
|
|
1146
1080
|
|
|
1147
1081
|
/**
|
|
1148
|
-
|
|
1149
|
-
|
|
1082
|
+
* Returns a read transaction
|
|
1083
|
+
*/
|
|
1150
1084
|
getReadTransaction() {
|
|
1151
1085
|
return this.transactionManager.getReadTransaction();
|
|
1152
1086
|
}
|
|
1153
1087
|
|
|
1154
1088
|
/**
|
|
1155
|
-
|
|
1156
|
-
|
|
1089
|
+
* Returns a write transaction
|
|
1090
|
+
*/
|
|
1157
1091
|
getWriteTransaction() {
|
|
1158
1092
|
return this.transactionManager.getWriteTransaction();
|
|
1159
1093
|
}
|
|
1160
1094
|
|
|
1161
1095
|
/**
|
|
1162
|
-
|
|
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
|
|
1102
|
+
console.warn('SyntropyFront: Error closing database:', closeResult.error);
|
|
1103
|
+
return false;
|
|
1169
1104
|
}
|
|
1170
|
-
|
|
1171
|
-
return
|
|
1105
|
+
|
|
1106
|
+
return true;
|
|
1172
1107
|
}
|
|
1173
1108
|
|
|
1174
1109
|
/**
|
|
1175
|
-
|
|
1176
|
-
|
|
1110
|
+
* Returns whether the database is available
|
|
1111
|
+
*/
|
|
1177
1112
|
isDatabaseAvailable() {
|
|
1178
1113
|
return this.connectionManager.isDatabaseAvailable();
|
|
1179
1114
|
}
|
|
1180
1115
|
|
|
1181
|
-
// =====
|
|
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 -
|
|
1221
|
-
*
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1252
|
-
|
|
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
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
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
|
-
// =====
|
|
1249
|
+
// ===== Private declarative methods =====
|
|
1315
1250
|
|
|
1316
1251
|
/**
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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 -
|
|
1394
|
-
*
|
|
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
|
-
*
|
|
1404
|
-
* @param {Function} sendCallback - Callback
|
|
1405
|
-
* @param {Function} removeCallback - Callback
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
1378
|
+
console.log(`SyntropyFront: Retry successful for item ${item.id}`);
|
|
1444
1379
|
} catch (error) {
|
|
1445
|
-
console.warn(`SyntropyFront:
|
|
1380
|
+
console.warn(`SyntropyFront: Retry failed for item ${item.id}:`, error);
|
|
1446
1381
|
|
|
1447
|
-
//
|
|
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}
|
|
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
|
|
1392
|
+
console.error('SyntropyFront: Error processing retries:', error);
|
|
1458
1393
|
}
|
|
1459
1394
|
}
|
|
1460
1395
|
|
|
1461
1396
|
/**
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
|
1409
|
+
console.error('SyntropyFront: Error incrementing retry count:', error);
|
|
1475
1410
|
}
|
|
1476
1411
|
}
|
|
1477
1412
|
|
|
1478
1413
|
/**
|
|
1479
|
-
|
|
1480
|
-
|
|
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
|
|
1421
|
+
console.error('SyntropyFront: Error removing failed item:', error);
|
|
1487
1422
|
}
|
|
1488
1423
|
}
|
|
1489
1424
|
|
|
1490
1425
|
/**
|
|
1491
|
-
|
|
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}
|
|
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:
|
|
1439
|
+
console.log(`SyntropyFront: Cleanup completed, ${expiredItems.length} items removed`);
|
|
1505
1440
|
}
|
|
1506
1441
|
} catch (error) {
|
|
1507
|
-
console.error('SyntropyFront: Error
|
|
1442
|
+
console.error('SyntropyFront: Error cleaning up expired items:', error);
|
|
1508
1443
|
}
|
|
1509
1444
|
}
|
|
1510
1445
|
|
|
1511
1446
|
/**
|
|
1512
|
-
|
|
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
|
|
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
|
-
*
|
|
1548
|
-
|
|
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
|
-
*
|
|
1557
|
-
* @param {Array} items - Items
|
|
1558
|
-
* @returns {Object}
|
|
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
|
-
|
|
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
|
-
|
|
1686
|
+
success: false,
|
|
1687
|
+
data: this.createFallbackData(error),
|
|
1578
1688
|
error: this.createSerializationError(error),
|
|
1579
|
-
|
|
1689
|
+
timestamp: new Date().toISOString()
|
|
1580
1690
|
};
|
|
1581
1691
|
}
|
|
1582
1692
|
}
|
|
1583
1693
|
|
|
1584
1694
|
/**
|
|
1585
|
-
*
|
|
1695
|
+
* Deserializes data with declarative error handling
|
|
1586
1696
|
* @param {string} serializedData - Datos serializados
|
|
1587
|
-
* @returns {Object}
|
|
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
|
-
|
|
1716
|
+
success: false,
|
|
1717
|
+
data: [],
|
|
1607
1718
|
error: this.createDeserializationError(error),
|
|
1608
|
-
|
|
1719
|
+
timestamp: new Date().toISOString()
|
|
1609
1720
|
};
|
|
1610
1721
|
}
|
|
1611
1722
|
}
|
|
1612
1723
|
|
|
1613
1724
|
/**
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
*
|
|
1643
|
-
* @param {Error} error - Error
|
|
1644
|
-
* @returns {string}
|
|
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
|
|
1762
|
+
fallbackData: 'Items not serializable - using fallback'
|
|
1652
1763
|
};
|
|
1653
1764
|
|
|
1654
1765
|
return JSON.stringify(fallbackPayload);
|
|
1655
1766
|
}
|
|
1656
1767
|
|
|
1657
1768
|
/**
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
-
*
|
|
1668
|
-
* @param {Object} result -
|
|
1669
|
-
* @param {*} fallback -
|
|
1670
|
-
* @returns {*}
|
|
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 -
|
|
1679
|
-
*
|
|
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
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
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.
|
|
1694
|
-
this.
|
|
1695
|
-
|
|
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
|
-
*
|
|
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:
|
|
1824
|
+
console.log('SyntropyFront: Persistent buffer initialized');
|
|
1710
1825
|
}
|
|
1711
1826
|
} catch (error) {
|
|
1712
|
-
console.warn('SyntropyFront: Error
|
|
1827
|
+
console.warn('SyntropyFront: Error initializing persistent buffer:', error);
|
|
1713
1828
|
}
|
|
1714
1829
|
}
|
|
1715
1830
|
|
|
1716
1831
|
/**
|
|
1717
|
-
*
|
|
1718
|
-
* @param {Array} items - Items
|
|
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
|
|
1842
|
+
console.log('SyntropyFront: Items saved to persistent buffer');
|
|
1728
1843
|
} catch (error) {
|
|
1729
|
-
console.error('SyntropyFront: Error
|
|
1844
|
+
console.error('SyntropyFront: Error saving to persistent buffer:', error);
|
|
1730
1845
|
}
|
|
1731
1846
|
}
|
|
1732
1847
|
|
|
1733
1848
|
/**
|
|
1734
|
-
*
|
|
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
|
|
1859
|
+
console.error('SyntropyFront: Error retrieving from persistent buffer:', error);
|
|
1745
1860
|
return [];
|
|
1746
1861
|
}
|
|
1747
1862
|
}
|
|
1748
1863
|
|
|
1749
1864
|
/**
|
|
1750
|
-
*
|
|
1751
|
-
* @param {number} id - ID
|
|
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
|
|
1876
|
+
console.error('SyntropyFront: Error removing from persistent buffer:', error);
|
|
1762
1877
|
}
|
|
1763
1878
|
}
|
|
1764
1879
|
|
|
1765
1880
|
/**
|
|
1766
|
-
*
|
|
1767
|
-
* @param {Function} sendCallback - Callback
|
|
1768
|
-
* @param {Function} removeCallback - Callback
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
1951
|
+
console.log('SyntropyFront: Persistent buffer cleared');
|
|
1837
1952
|
} catch (error) {
|
|
1838
|
-
console.error('SyntropyFront: Error
|
|
1953
|
+
console.error('SyntropyFront: Error clearing persistent buffer:', error);
|
|
1839
1954
|
}
|
|
1840
1955
|
}
|
|
1841
1956
|
|
|
1842
1957
|
/**
|
|
1843
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1861
|
-
*
|
|
1862
|
-
*
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
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
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
this.
|
|
1879
|
-
this.
|
|
1880
|
-
|
|
1881
|
-
//
|
|
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
|
-
|
|
1887
|
-
|
|
1997
|
+
* Configures callbacks for coordination between components
|
|
1998
|
+
*/
|
|
1888
1999
|
setupCallbacks() {
|
|
1889
|
-
//
|
|
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:
|
|
2004
|
+
console.log('SyntropyFront: Data sent successfully');
|
|
1894
2005
|
} catch (error) {
|
|
1895
|
-
console.error('SyntropyFront Agent: Error
|
|
1896
|
-
|
|
1897
|
-
//
|
|
2006
|
+
console.error('SyntropyFront Agent: Error sending data:', error);
|
|
2007
|
+
|
|
2008
|
+
// Add to retry queue
|
|
1898
2009
|
this.retry.addToRetryQueue(items);
|
|
1899
|
-
|
|
1900
|
-
//
|
|
2010
|
+
|
|
2011
|
+
// Save to persistent buffer
|
|
1901
2012
|
await this.buffer.save(items);
|
|
1902
2013
|
}
|
|
1903
2014
|
};
|
|
1904
2015
|
|
|
1905
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
1924
|
-
|
|
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
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
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:
|
|
2049
|
+
console.warn('SyntropyFront Agent: Not configured, error not sent');
|
|
1938
2050
|
return;
|
|
1939
2051
|
}
|
|
1940
2052
|
|
|
1941
|
-
//
|
|
1942
|
-
|
|
1943
|
-
...errorPayload,
|
|
1944
|
-
context
|
|
1945
|
-
} : errorPayload;
|
|
2053
|
+
// 🎲 Guard: Sampling
|
|
2054
|
+
if (Math.random() > this.config.samplingRate) return;
|
|
1946
2055
|
|
|
1947
|
-
//
|
|
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:
|
|
2067
|
+
data: maskedData,
|
|
1953
2068
|
timestamp: new Date().toISOString()
|
|
1954
2069
|
});
|
|
1955
2070
|
}
|
|
1956
2071
|
|
|
1957
2072
|
/**
|
|
1958
|
-
|
|
1959
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
2092
|
+
data: maskedData,
|
|
1973
2093
|
timestamp: new Date().toISOString()
|
|
1974
2094
|
});
|
|
1975
2095
|
}
|
|
1976
2096
|
|
|
1977
2097
|
/**
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
-
|
|
1987
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2014
|
-
|
|
2129
|
+
* Forces immediate flush of everything
|
|
2130
|
+
*/
|
|
2015
2131
|
async forceFlush() {
|
|
2016
2132
|
await this.flush();
|
|
2017
|
-
|
|
2018
|
-
//
|
|
2133
|
+
|
|
2134
|
+
// Also attempt pending retries
|
|
2019
2135
|
if (!this.retry.isEmpty()) {
|
|
2020
|
-
console.log('SyntropyFront:
|
|
2136
|
+
console.log('SyntropyFront: Attempting to send pending retries...');
|
|
2021
2137
|
await this.processRetryQueue();
|
|
2022
2138
|
}
|
|
2023
2139
|
}
|
|
2024
2140
|
|
|
2025
2141
|
/**
|
|
2026
|
-
|
|
2027
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|
|
2180
|
+
this.config.configure({ endpoint: null });
|
|
2052
2181
|
this.queue.clear();
|
|
2053
2182
|
this.retry.clear();
|
|
2054
2183
|
}
|
|
2055
2184
|
}
|
|
2056
2185
|
|
|
2057
|
-
//
|
|
2186
|
+
// Singleton Instance
|
|
2058
2187
|
const agent = new Agent();
|
|
2059
2188
|
|
|
2060
2189
|
/**
|
|
2061
|
-
*
|
|
2062
|
-
*
|
|
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
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2195
|
-
const context = {};
|
|
2202
|
+
getGlobal: () => (typeof window !== 'undefined' ? window : {}),
|
|
2196
2203
|
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
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
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2252
|
-
|
|
2253
|
-
if (!
|
|
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
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
|
2290
|
+
return `${tag}${id}${classes}`;
|
|
2273
2291
|
}
|
|
2292
|
+
};
|
|
2274
2293
|
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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
|
-
*
|
|
2314
|
+
* Pipeline de procesamiento de clic (Pureza funcional).
|
|
2315
2315
|
*/
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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
|
-
*
|
|
2325
|
-
* @returns {Array} Tipos disponibles
|
|
2329
|
+
* Flow control to avoid duplicates.
|
|
2326
2330
|
*/
|
|
2327
|
-
|
|
2328
|
-
|
|
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
|
-
*
|
|
2333
|
-
* @param {string} contextType - Tipo de contexto
|
|
2334
|
-
* @returns {Array} Campos disponibles
|
|
2339
|
+
* Records trace in the store.
|
|
2335
2340
|
*/
|
|
2336
|
-
|
|
2337
|
-
const
|
|
2338
|
-
|
|
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
|
-
*
|
|
2343
|
-
* @returns {Object} Información de sets por defecto
|
|
2358
|
+
* Limpieza de recursos.
|
|
2344
2359
|
*/
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
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
|
-
*
|
|
2376
|
-
*
|
|
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
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
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
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
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
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2428
|
+
/**
|
|
2429
|
+
* Copyright 2024 Syntropysoft
|
|
2430
|
+
*/
|
|
2399
2431
|
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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
|
-
*
|
|
2443
|
+
* Initializes interception of the global fetch API.
|
|
2411
2444
|
*/
|
|
2412
2445
|
init() {
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
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
|
-
|
|
2431
|
-
|
|
2465
|
+
// Continue with original execution
|
|
2466
|
+
return original.apply(window, args);
|
|
2467
|
+
};
|
|
2468
|
+
});
|
|
2432
2469
|
}
|
|
2433
2470
|
|
|
2434
2471
|
/**
|
|
2435
|
-
*
|
|
2472
|
+
* Restores original fetch behaviour.
|
|
2436
2473
|
*/
|
|
2437
|
-
|
|
2438
|
-
//
|
|
2439
|
-
if (
|
|
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
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2478
|
+
this.wrapper.destroy();
|
|
2479
|
+
this.wrapper = null;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2450
2482
|
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
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
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
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
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Collects context based on a declarative configuration.
|
|
2620
|
+
*/
|
|
2621
|
+
collect(contextConfig = {}) {
|
|
2622
|
+
const config = this.normalizeConfig(contextConfig);
|
|
2478
2623
|
|
|
2479
|
-
|
|
2480
|
-
|
|
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
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
}
|
|
2487
|
-
|
|
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
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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
|
-
|
|
2504
|
-
|
|
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
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
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
|
-
|
|
2519
|
-
|
|
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
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
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
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2693
|
+
generateSessionId() {
|
|
2694
|
+
this.sessionId = this.sessionId || `session_${this.generateSecureId()}`;
|
|
2695
|
+
return this.sessionId;
|
|
2696
|
+
}
|
|
2539
2697
|
|
|
2540
|
-
|
|
2541
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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
|
-
|
|
2714
|
+
get defaultContexts() {
|
|
2715
|
+
return this.allFields;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2574
2718
|
|
|
2575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2589
|
-
|
|
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
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
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
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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
|
-
|
|
2772
|
+
this.setupExceptionCapture();
|
|
2773
|
+
this.setupRejectionCapture();
|
|
2774
|
+
}
|
|
2609
2775
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
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
|
-
|
|
2621
|
-
|
|
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
|
-
*
|
|
2627
|
-
* @param {Object} errorPayload - Payload del error
|
|
2793
|
+
* Sets up the wrapper for promise rejections.
|
|
2628
2794
|
*/
|
|
2629
|
-
|
|
2630
|
-
//
|
|
2631
|
-
|
|
2795
|
+
setupRejectionCapture() {
|
|
2796
|
+
// Guard: capture disabled
|
|
2797
|
+
if (!this.config.captureUnhandledRejections) return;
|
|
2632
2798
|
|
|
2633
|
-
|
|
2634
|
-
|
|
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
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
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
|
-
*
|
|
2818
|
+
* Cleans up wrappers to avoid memory leaks.
|
|
2647
2819
|
*/
|
|
2648
2820
|
destroy() {
|
|
2649
|
-
|
|
2821
|
+
this.errorWrapper?.destroy();
|
|
2822
|
+
this.rejectionWrapper?.destroy();
|
|
2823
|
+
this.errorWrapper = null;
|
|
2824
|
+
this.rejectionWrapper = null;
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2650
2827
|
|
|
2651
|
-
|
|
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
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
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
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
}
|
|
2848
|
+
this.registry = new Map();
|
|
2849
|
+
this.initializeRegistry();
|
|
2850
|
+
}
|
|
2668
2851
|
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
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
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
this.
|
|
2684
|
-
|
|
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
|
-
|
|
2877
|
+
this.runLifecycle('init');
|
|
2878
|
+
|
|
2879
|
+
this.isInitialized = true;
|
|
2880
|
+
console.log('SyntropyFront: Interceptors initialized (Refactored architecture)');
|
|
2687
2881
|
}
|
|
2688
2882
|
|
|
2689
2883
|
/**
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
return
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
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-
|
|
2955
|
+
// Auto-initialize
|
|
2740
2956
|
this.init();
|
|
2741
2957
|
}
|
|
2742
2958
|
|
|
2743
2959
|
/**
|
|
2744
|
-
*
|
|
2960
|
+
* Initializes the library and activates interceptors
|
|
2745
2961
|
*/
|
|
2746
2962
|
init() {
|
|
2747
2963
|
if (this.isActive) return;
|
|
2748
2964
|
|
|
2749
|
-
|
|
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
|
-
//
|
|
2968
|
+
// Retry failed items from previous sessions
|
|
2772
2969
|
agent.retryFailedItems().catch(err => {
|
|
2773
|
-
console.warn('SyntropyFront: Error
|
|
2970
|
+
console.warn('SyntropyFront: Error attempting to recover persistent items:', err);
|
|
2774
2971
|
});
|
|
2775
2972
|
|
|
2776
2973
|
this.isActive = true;
|
|
2777
|
-
console.log('🚀 SyntropyFront:
|
|
2974
|
+
console.log('🚀 SyntropyFront: Initialized with modular resilient architecture');
|
|
2778
2975
|
}
|
|
2779
2976
|
|
|
2780
2977
|
/**
|
|
2781
|
-
*
|
|
2782
|
-
* @param {Object} config - Configuración
|
|
2978
|
+
* Private: applies current config to agent and interceptors.
|
|
2783
2979
|
*/
|
|
2784
|
-
|
|
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
|
-
|
|
2809
|
-
|
|
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:
|
|
3020
|
+
console.log(`✅ SyntropyFront: Configured - ${mode}`);
|
|
2814
3021
|
}
|
|
2815
3022
|
|
|
2816
3023
|
/**
|
|
2817
|
-
*
|
|
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
|
-
*
|
|
3031
|
+
* Gets all breadcrumbs
|
|
2825
3032
|
*/
|
|
2826
3033
|
getBreadcrumbs() {
|
|
2827
3034
|
return breadcrumbStore.getAll();
|
|
2828
3035
|
}
|
|
2829
3036
|
|
|
2830
3037
|
/**
|
|
2831
|
-
*
|
|
3038
|
+
* Clears breadcrumbs
|
|
2832
3039
|
*/
|
|
2833
3040
|
clearBreadcrumbs() {
|
|
2834
3041
|
breadcrumbStore.clear();
|
|
2835
3042
|
}
|
|
2836
3043
|
|
|
2837
3044
|
/**
|
|
2838
|
-
*
|
|
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
|
-
*
|
|
3064
|
+
* Forces sending pending data
|
|
2858
3065
|
*/
|
|
2859
3066
|
async flush() {
|
|
2860
3067
|
await agent.forceFlush();
|
|
2861
3068
|
}
|
|
2862
3069
|
|
|
2863
3070
|
/**
|
|
2864
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
3089
|
+
console.log('SyntropyFront: Deactivated');
|
|
2883
3090
|
}
|
|
2884
3091
|
}
|
|
2885
3092
|
|
|
2886
|
-
//
|
|
3093
|
+
// Singleton instance
|
|
2887
3094
|
const syntropyFront = new SyntropyFront();
|
|
2888
3095
|
|
|
2889
3096
|
exports.default = syntropyFront;
|