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