crawlforge-mcp-server 3.0.0

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.
Files changed (75) hide show
  1. package/CLAUDE.md +315 -0
  2. package/LICENSE +21 -0
  3. package/README.md +181 -0
  4. package/package.json +115 -0
  5. package/server.js +1963 -0
  6. package/setup.js +112 -0
  7. package/src/constants/config.js +615 -0
  8. package/src/core/ActionExecutor.js +1104 -0
  9. package/src/core/AlertNotificationSystem.js +601 -0
  10. package/src/core/AuthManager.js +315 -0
  11. package/src/core/ChangeTracker.js +2306 -0
  12. package/src/core/JobManager.js +687 -0
  13. package/src/core/LLMsTxtAnalyzer.js +753 -0
  14. package/src/core/LocalizationManager.js +1615 -0
  15. package/src/core/PerformanceManager.js +828 -0
  16. package/src/core/ResearchOrchestrator.js +1327 -0
  17. package/src/core/SnapshotManager.js +1037 -0
  18. package/src/core/StealthBrowserManager.js +1795 -0
  19. package/src/core/WebhookDispatcher.js +745 -0
  20. package/src/core/analysis/ContentAnalyzer.js +749 -0
  21. package/src/core/analysis/LinkAnalyzer.js +972 -0
  22. package/src/core/cache/CacheManager.js +821 -0
  23. package/src/core/connections/ConnectionPool.js +553 -0
  24. package/src/core/crawlers/BFSCrawler.js +845 -0
  25. package/src/core/integrations/PerformanceIntegration.js +377 -0
  26. package/src/core/llm/AnthropicProvider.js +135 -0
  27. package/src/core/llm/LLMManager.js +415 -0
  28. package/src/core/llm/LLMProvider.js +97 -0
  29. package/src/core/llm/OpenAIProvider.js +127 -0
  30. package/src/core/processing/BrowserProcessor.js +986 -0
  31. package/src/core/processing/ContentProcessor.js +505 -0
  32. package/src/core/processing/PDFProcessor.js +448 -0
  33. package/src/core/processing/StreamProcessor.js +673 -0
  34. package/src/core/queue/QueueManager.js +98 -0
  35. package/src/core/workers/WorkerPool.js +585 -0
  36. package/src/core/workers/worker.js +743 -0
  37. package/src/monitoring/healthCheck.js +600 -0
  38. package/src/monitoring/metrics.js +761 -0
  39. package/src/optimization/wave3-optimizations.js +932 -0
  40. package/src/security/security-patches.js +120 -0
  41. package/src/security/security-tests.js +355 -0
  42. package/src/security/wave3-security.js +652 -0
  43. package/src/tools/advanced/BatchScrapeTool.js +1089 -0
  44. package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
  45. package/src/tools/crawl/crawlDeep.js +449 -0
  46. package/src/tools/crawl/mapSite.js +400 -0
  47. package/src/tools/extract/analyzeContent.js +624 -0
  48. package/src/tools/extract/extractContent.js +329 -0
  49. package/src/tools/extract/processDocument.js +503 -0
  50. package/src/tools/extract/summarizeContent.js +376 -0
  51. package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
  52. package/src/tools/research/deepResearch.js +706 -0
  53. package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
  54. package/src/tools/search/adapters/googleSearch.js +236 -0
  55. package/src/tools/search/adapters/searchProviderFactory.js +96 -0
  56. package/src/tools/search/queryExpander.js +543 -0
  57. package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
  58. package/src/tools/search/ranking/ResultRanker.js +497 -0
  59. package/src/tools/search/searchWeb.js +482 -0
  60. package/src/tools/tracking/trackChanges.js +1355 -0
  61. package/src/utils/CircuitBreaker.js +515 -0
  62. package/src/utils/ErrorHandlingConfig.js +342 -0
  63. package/src/utils/HumanBehaviorSimulator.js +569 -0
  64. package/src/utils/Logger.js +568 -0
  65. package/src/utils/MemoryMonitor.js +173 -0
  66. package/src/utils/RetryManager.js +386 -0
  67. package/src/utils/contentUtils.js +588 -0
  68. package/src/utils/domainFilter.js +612 -0
  69. package/src/utils/inputValidation.js +766 -0
  70. package/src/utils/rateLimiter.js +196 -0
  71. package/src/utils/robotsChecker.js +91 -0
  72. package/src/utils/securityMiddleware.js +416 -0
  73. package/src/utils/sitemapParser.js +678 -0
  74. package/src/utils/ssrfProtection.js +640 -0
  75. package/src/utils/urlNormalizer.js +168 -0
@@ -0,0 +1,745 @@
1
+ /**
2
+ * WebhookDispatcher - Webhook infrastructure for event notifications
3
+ * Features: queuing, retry logic, HMAC security, health monitoring
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+ import { EventEmitter } from 'events';
8
+ import { promises as fs } from 'fs';
9
+ import path from 'path';
10
+ import RetryManager from '../utils/RetryManager.js';
11
+
12
+ export class WebhookDispatcher extends EventEmitter {
13
+ constructor(options = {}) {
14
+ super();
15
+
16
+ const {
17
+ queueDir = './webhooks',
18
+ maxRetries = 3,
19
+ retryDelay = 5000,
20
+ timeout = 30000,
21
+ enablePersistence = true,
22
+ maxQueueSize = 10000,
23
+ healthCheckInterval = 60000, // 1 minute
24
+ enableHealthMonitoring = true,
25
+ batchSize = 50,
26
+ enableBatching = false,
27
+ signingSecret = null,
28
+ defaultHeaders = {},
29
+ enableLogging = true
30
+ } = options;
31
+
32
+ this.queueDir = queueDir;
33
+ this.maxRetries = maxRetries;
34
+ this.retryDelay = retryDelay;
35
+ this.timeout = timeout;
36
+ this.enablePersistence = enablePersistence;
37
+ this.maxQueueSize = maxQueueSize;
38
+ this.healthCheckInterval = healthCheckInterval;
39
+ this.enableHealthMonitoring = enableHealthMonitoring;
40
+ this.batchSize = batchSize;
41
+ this.enableBatching = enableBatching;
42
+ this.signingSecret = signingSecret;
43
+ this.defaultHeaders = defaultHeaders;
44
+ this.enableLogging = enableLogging;
45
+
46
+ // Webhook queue - in memory and persistent
47
+ this.queue = [];
48
+ this.processing = false;
49
+ this.webhookUrls = new Map(); // url -> configuration
50
+ this.failedUrls = new Map(); // url -> failure info
51
+
52
+ // Statistics
53
+ this.stats = {
54
+ totalEvents: 0,
55
+ successfulDeliveries: 0,
56
+ failedDeliveries: 0,
57
+ retriedDeliveries: 0,
58
+ averageResponseTime: 0,
59
+ healthyUrls: 0,
60
+ unhealthyUrls: 0,
61
+ lastUpdated: Date.now()
62
+ };
63
+
64
+ // Health monitoring
65
+ this.healthChecks = new Map(); // url -> health status
66
+ this.responseTimes = new Map(); // url -> response time history
67
+
68
+ // Retry manager
69
+ this.retryManager = new RetryManager({
70
+ maxRetries: this.maxRetries,
71
+ baseDelay: this.retryDelay,
72
+ strategy: 'exponential',
73
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504],
74
+ onRetry: (error, attempt, delay, context) => {
75
+ this.stats.retriedDeliveries++;
76
+ if (this.enableLogging) {
77
+ console.log('Webhook retry ' + attempt + ' for ' + context.url + ' after ' + delay + 'ms: ' + error.message);
78
+ }
79
+ }
80
+ });
81
+
82
+ // Initialize storage
83
+ if (this.enablePersistence) {
84
+ this.initStorage();
85
+ }
86
+
87
+ // Start health monitoring
88
+ if (this.enableHealthMonitoring) {
89
+ this.startHealthMonitoring();
90
+ }
91
+
92
+ // Start processing queue
93
+ this.startProcessing();
94
+ }
95
+
96
+ /**
97
+ * Initialize persistent storage
98
+ */
99
+ async initStorage() {
100
+ try {
101
+ await fs.mkdir(this.queueDir, { recursive: true });
102
+ await this.loadPersistedEvents();
103
+ } catch (error) {
104
+ console.error('Failed to initialize webhook storage:', error);
105
+ this.enablePersistence = false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Register webhook URL with configuration
111
+ * @param {string|Object} urlOrConfig - Webhook URL string or config object with url property
112
+ * @param {Object} config - Webhook configuration (when first param is URL string)
113
+ */
114
+ registerWebhook(urlOrConfig, config = {}) {
115
+ let url, actualConfig;
116
+
117
+ // Handle both signatures: registerWebhook(url, config) and registerWebhook({url, ...config})
118
+ if (typeof urlOrConfig === 'string') {
119
+ url = urlOrConfig;
120
+ actualConfig = config;
121
+ } else if (urlOrConfig && typeof urlOrConfig === 'object' && urlOrConfig.url) {
122
+ url = urlOrConfig.url;
123
+ actualConfig = { ...urlOrConfig };
124
+ delete actualConfig.url; // Remove url from config since it's handled separately
125
+ } else {
126
+ throw new Error('Invalid webhook configuration: URL is required');
127
+ }
128
+
129
+ const webhookConfig = {
130
+ id: this.generateEventId(), // Add unique ID
131
+ url,
132
+ enabled: actualConfig.enabled !== false,
133
+ events: actualConfig.events || ['*'], // * means all events
134
+ headers: Object.assign({}, this.defaultHeaders, actualConfig.headers || {}),
135
+ timeout: actualConfig.timeout || this.timeout,
136
+ maxRetries: actualConfig.maxRetries || this.maxRetries,
137
+ retryDelay: actualConfig.retryDelay || this.retryDelay,
138
+ signingSecret: actualConfig.signingSecret || actualConfig.secret || this.signingSecret, // support 'secret' alias
139
+ metadata: actualConfig.metadata || {},
140
+ createdAt: Date.now(),
141
+ lastUsed: null
142
+ };
143
+
144
+ this.webhookUrls.set(url, webhookConfig);
145
+ this.healthChecks.set(url, {
146
+ status: 'unknown',
147
+ lastCheck: null,
148
+ consecutiveFailures: 0,
149
+ averageResponseTime: 0,
150
+ lastError: null
151
+ });
152
+
153
+ this.emit('webhookRegistered', url, webhookConfig);
154
+ return webhookConfig;
155
+ }
156
+
157
+ /**
158
+ * Unregister webhook URL
159
+ * @param {string} url - Webhook URL
160
+ */
161
+ unregisterWebhook(url) {
162
+ const removed = this.webhookUrls.delete(url);
163
+ this.healthChecks.delete(url);
164
+ this.responseTimes.delete(url);
165
+ this.failedUrls.delete(url);
166
+
167
+ if (removed) {
168
+ this.emit('webhookUnregistered', url);
169
+ }
170
+ return removed;
171
+ }
172
+
173
+ /**
174
+ * Dispatch webhook event
175
+ * @param {string} eventType - Event type
176
+ * @param {Object} payload - Event payload
177
+ * @param {Object} options - Dispatch options
178
+ * @returns {Promise<Array>} Array of dispatch results
179
+ */
180
+ async dispatch(eventType, payload, options = {}) {
181
+ const {
182
+ urls = null, // Specific URLs to send to, null for all registered
183
+ immediate = false, // Skip queue and send immediately
184
+ priority = 0, // Higher number = higher priority
185
+ metadata = {}
186
+ } = options;
187
+
188
+ const event = {
189
+ id: this.generateEventId(),
190
+ type: eventType,
191
+ payload,
192
+ timestamp: Date.now(),
193
+ priority,
194
+ metadata,
195
+ attempts: 0,
196
+ createdAt: Date.now()
197
+ };
198
+
199
+ this.stats.totalEvents++;
200
+
201
+ // Determine target URLs
202
+ const targetUrls = urls || Array.from(this.webhookUrls.keys());
203
+ const filteredUrls = targetUrls.filter(url => {
204
+ const config = this.webhookUrls.get(url);
205
+ return config &&
206
+ config.enabled &&
207
+ (config.events.includes('*') || config.events.includes(eventType));
208
+ });
209
+
210
+ if (filteredUrls.length === 0) {
211
+ this.emit('noTargets', event, eventType);
212
+ return [];
213
+ }
214
+
215
+ // Create dispatch tasks
216
+ const tasks = filteredUrls.map(url => {
217
+ const urlString = typeof url === 'string' ? url : String(url);
218
+ const cleanUrl = urlString.replace(/[^a-zA-Z0-9]/g, '_');
219
+ return Object.assign({}, event, {
220
+ url: urlString,
221
+ id: event.id + '_' + cleanUrl
222
+ });
223
+ });
224
+
225
+ if (immediate) {
226
+ // Process immediately
227
+ const results = [];
228
+ for (const task of tasks) {
229
+ try {
230
+ const result = await this.deliverWebhook(task);
231
+ results.push(result);
232
+ } catch (error) {
233
+ results.push({
234
+ success: false,
235
+ url: task.url,
236
+ error: error.message,
237
+ timestamp: Date.now()
238
+ });
239
+ }
240
+ }
241
+ return results;
242
+ } else {
243
+ // Add to queue
244
+ await this.enqueueEvents(tasks);
245
+ this.emit('eventsQueued', tasks.length, eventType);
246
+ return tasks.map(t => ({ queued: true, url: t.url, eventId: t.id }));
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Add events to queue
252
+ * @param {Array} events - Events to queue
253
+ */
254
+ async enqueueEvents(events) {
255
+ // Check queue size limit
256
+ if (this.queue.length + events.length > this.maxQueueSize) {
257
+ const excess = (this.queue.length + events.length) - this.maxQueueSize;
258
+ // Remove oldest events to make room
259
+ this.queue.splice(0, excess);
260
+ this.emit('queueOverflow', excess);
261
+ }
262
+
263
+ // Add to queue with priority sorting
264
+ this.queue.push(...events);
265
+ this.queue.sort((a, b) => b.priority - a.priority);
266
+
267
+ // Persist if enabled
268
+ if (this.enablePersistence) {
269
+ await this.persistQueue();
270
+ }
271
+
272
+ this.emit('eventsEnqueued', events.length);
273
+ }
274
+
275
+ /**
276
+ * Process webhook queue
277
+ */
278
+ async processQueue() {
279
+ if (this.processing || this.queue.length === 0) {
280
+ return;
281
+ }
282
+
283
+ this.processing = true;
284
+
285
+ try {
286
+ const batchSize = this.enableBatching ? this.batchSize : 1;
287
+ const batch = this.queue.splice(0, batchSize);
288
+
289
+ if (this.enableBatching && batch.length > 1) {
290
+ await this.processBatch(batch);
291
+ } else {
292
+ for (const event of batch) {
293
+ await this.processEvent(event);
294
+ }
295
+ }
296
+
297
+ // Update persistence after processing
298
+ if (this.enablePersistence && batch.length > 0) {
299
+ await this.persistQueue();
300
+ }
301
+
302
+ } catch (error) {
303
+ console.error('Queue processing error:', error);
304
+ this.emit('processingError', error);
305
+ } finally {
306
+ this.processing = false;
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Process individual webhook event
312
+ * @param {Object} event - Webhook event
313
+ */
314
+ async processEvent(event) {
315
+ try {
316
+ event.attempts++;
317
+ const result = await this.deliverWebhook(event);
318
+
319
+ this.stats.successfulDeliveries++;
320
+ this.emit('webhookDelivered', event, result);
321
+
322
+ } catch (error) {
323
+ event.lastError = error.message;
324
+
325
+ // Check if we should retry
326
+ if (event.attempts < this.maxRetries) {
327
+ // Re-queue for retry with exponential backoff
328
+ const delay = Math.min(
329
+ this.retryDelay * Math.pow(2, event.attempts - 1),
330
+ 60000 // Max 1 minute delay
331
+ );
332
+
333
+ setTimeout(() => {
334
+ this.queue.unshift(event); // Add to front for priority
335
+ }, delay);
336
+
337
+ this.emit('webhookRetry', event, error, delay);
338
+ } else {
339
+ this.stats.failedDeliveries++;
340
+ this.recordFailure(event.url, error);
341
+ this.emit('webhookFailed', event, error);
342
+ }
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Process batch of webhook events
348
+ * @param {Array} batch - Batch of events
349
+ */
350
+ async processBatch(batch) {
351
+ const promises = batch.map(event => this.processEvent(event));
352
+ await Promise.allSettled(promises);
353
+ }
354
+
355
+ /**
356
+ * Deliver webhook to URL
357
+ * @param {Object} event - Webhook event
358
+ * @returns {Promise<Object>} Delivery result
359
+ */
360
+ async deliverWebhook(event) {
361
+ const config = this.webhookUrls.get(event.url);
362
+ if (!config) {
363
+ throw new Error('Webhook URL ' + event.url + ' not registered');
364
+ }
365
+
366
+ const startTime = Date.now();
367
+ const headers = Object.assign({}, config.headers);
368
+
369
+ // Add standard headers
370
+ headers['Content-Type'] = 'application/json';
371
+ headers['User-Agent'] = 'WebhookDispatcher/1.0';
372
+ headers['X-Webhook-Event'] = event.type;
373
+ headers['X-Webhook-ID'] = event.id;
374
+ headers['X-Webhook-Timestamp'] = event.timestamp.toString();
375
+
376
+ // Add HMAC signature if secret provided
377
+ if (config.signingSecret) {
378
+ const signature = this.generateSignature(event.payload, config.signingSecret);
379
+ headers['X-Webhook-Signature'] = signature;
380
+ }
381
+
382
+ // Create request body
383
+ const body = JSON.stringify({
384
+ event: event.type,
385
+ id: event.id,
386
+ timestamp: event.timestamp,
387
+ data: event.payload,
388
+ metadata: event.metadata
389
+ });
390
+
391
+ // Execute with retry logic
392
+ const result = await this.retryManager.execute(async () => {
393
+ const response = await fetch(event.url, {
394
+ method: 'POST',
395
+ headers,
396
+ body,
397
+ timeout: config.timeout
398
+ });
399
+
400
+ if (!response.ok) {
401
+ throw new Error('HTTP ' + response.status + ': ' + response.statusText);
402
+ }
403
+
404
+ return response;
405
+ }, { url: event.url });
406
+
407
+ const duration = Date.now() - startTime;
408
+ this.recordSuccess(event.url, duration);
409
+
410
+ // Update webhook last used time
411
+ config.lastUsed = Date.now();
412
+
413
+ return {
414
+ success: true,
415
+ url: event.url,
416
+ status: result.status,
417
+ duration,
418
+ timestamp: Date.now()
419
+ };
420
+ }
421
+
422
+ /**
423
+ * Generate HMAC signature for webhook security
424
+ * @param {Object} payload - Webhook payload
425
+ * @param {string} secret - Signing secret
426
+ * @returns {string} HMAC signature
427
+ */
428
+ generateSignature(payload, secret) {
429
+ const body = JSON.stringify(payload);
430
+ const hmac = crypto.createHmac('sha256', secret);
431
+ hmac.update(body);
432
+ return 'sha256=' + hmac.digest('hex');
433
+ }
434
+
435
+ /**
436
+ * Record successful delivery
437
+ * @param {string} url - Webhook URL
438
+ * @param {number} duration - Response time in milliseconds
439
+ */
440
+ recordSuccess(url, duration) {
441
+ const health = this.healthChecks.get(url);
442
+ if (health) {
443
+ health.status = 'healthy';
444
+ health.lastCheck = Date.now();
445
+ health.consecutiveFailures = 0;
446
+
447
+ // Update response time
448
+ if (!this.responseTimes.has(url)) {
449
+ this.responseTimes.set(url, []);
450
+ }
451
+ const times = this.responseTimes.get(url);
452
+ times.push(duration);
453
+ if (times.length > 100) times.shift(); // Keep last 100 measurements
454
+
455
+ health.averageResponseTime = times.reduce((a, b) => a + b, 0) / times.length;
456
+ }
457
+
458
+ // Update global stats
459
+ this.updateAverageResponseTime(duration);
460
+ this.updateStats();
461
+ }
462
+
463
+ /**
464
+ * Record delivery failure
465
+ * @param {string} url - Webhook URL
466
+ * @param {Error} error - Failure error
467
+ */
468
+ recordFailure(url, error) {
469
+ const health = this.healthChecks.get(url);
470
+ if (health) {
471
+ health.status = 'unhealthy';
472
+ health.lastCheck = Date.now();
473
+ health.consecutiveFailures++;
474
+ health.lastError = error.message;
475
+ }
476
+
477
+ // Track failed URLs
478
+ if (!this.failedUrls.has(url)) {
479
+ this.failedUrls.set(url, {
480
+ firstFailure: Date.now(),
481
+ failureCount: 0,
482
+ lastError: null
483
+ });
484
+ }
485
+
486
+ const failureInfo = this.failedUrls.get(url);
487
+ failureInfo.failureCount++;
488
+ failureInfo.lastError = error.message;
489
+ failureInfo.lastFailure = Date.now();
490
+
491
+ this.updateStats();
492
+ }
493
+
494
+ /**
495
+ * Start health monitoring
496
+ */
497
+ startHealthMonitoring() {
498
+ if (this.healthMonitoringTimer) {
499
+ clearInterval(this.healthMonitoringTimer);
500
+ }
501
+
502
+ this.healthMonitoringTimer = setInterval(() => {
503
+ this.performHealthChecks();
504
+ }, this.healthCheckInterval);
505
+ }
506
+
507
+ /**
508
+ * Perform health checks on all registered webhooks
509
+ */
510
+ async performHealthChecks() {
511
+ const urls = Array.from(this.webhookUrls.keys());
512
+ const healthCheckPromises = urls.map(url => this.healthCheckUrl(url));
513
+
514
+ try {
515
+ await Promise.allSettled(healthCheckPromises);
516
+ this.updateStats();
517
+ this.emit('healthCheckComplete', this.getHealthSummary());
518
+ } catch (error) {
519
+ this.emit('healthCheckError', error);
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Perform health check on specific URL
525
+ * @param {string} url - Webhook URL
526
+ */
527
+ async healthCheckUrl(url) {
528
+ const config = this.webhookUrls.get(url);
529
+ if (!config || !config.enabled) return;
530
+
531
+ try {
532
+ const startTime = Date.now();
533
+ const response = await fetch(url, {
534
+ method: 'HEAD',
535
+ timeout: config.timeout / 2, // Use half timeout for health checks
536
+ headers: {
537
+ 'User-Agent': 'WebhookDispatcher-HealthCheck/1.0'
538
+ }
539
+ });
540
+
541
+ const duration = Date.now() - startTime;
542
+
543
+ if (response.ok || response.status === 405) { // 405 Method Not Allowed is OK for HEAD
544
+ this.recordSuccess(url, duration);
545
+ } else {
546
+ throw new Error('HTTP ' + response.status + ': ' + response.statusText);
547
+ }
548
+
549
+ } catch (error) {
550
+ this.recordFailure(url, error);
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Start queue processing
556
+ */
557
+ startProcessing() {
558
+ if (this.processingTimer) {
559
+ clearInterval(this.processingTimer);
560
+ }
561
+
562
+ // Process queue every 100ms
563
+ this.processingTimer = setInterval(() => {
564
+ this.processQueue();
565
+ }, 100);
566
+ }
567
+
568
+ /**
569
+ * Stop processing
570
+ */
571
+ stopProcessing() {
572
+ if (this.processingTimer) {
573
+ clearInterval(this.processingTimer);
574
+ this.processingTimer = null;
575
+ }
576
+
577
+ if (this.healthMonitoringTimer) {
578
+ clearInterval(this.healthMonitoringTimer);
579
+ this.healthMonitoringTimer = null;
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Persist queue to disk
585
+ */
586
+ async persistQueue() {
587
+ if (!this.enablePersistence) return;
588
+
589
+ try {
590
+ const queuePath = path.join(this.queueDir, 'queue.json');
591
+ const data = JSON.stringify(this.queue, null, 2);
592
+ await fs.writeFile(queuePath, data, 'utf8');
593
+ } catch (error) {
594
+ console.error('Failed to persist webhook queue:', error);
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Load persisted events
600
+ */
601
+ async loadPersistedEvents() {
602
+ if (!this.enablePersistence) return;
603
+
604
+ try {
605
+ const queuePath = path.join(this.queueDir, 'queue.json');
606
+ const data = await fs.readFile(queuePath, 'utf8');
607
+ const events = JSON.parse(data);
608
+
609
+ if (Array.isArray(events)) {
610
+ this.queue = events;
611
+ this.emit('queueLoaded', events.length);
612
+ }
613
+ } catch (error) {
614
+ if (error.code !== 'ENOENT') {
615
+ console.error('Failed to load persisted webhook queue:', error);
616
+ }
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Update average response time statistic
622
+ * @param {number} responseTime - Response time in milliseconds
623
+ */
624
+ updateAverageResponseTime(responseTime) {
625
+ const currentAverage = this.stats.averageResponseTime;
626
+ const totalDeliveries = this.stats.successfulDeliveries;
627
+
628
+ if (totalDeliveries === 1) {
629
+ this.stats.averageResponseTime = responseTime;
630
+ } else {
631
+ this.stats.averageResponseTime =
632
+ ((currentAverage * (totalDeliveries - 1)) + responseTime) / totalDeliveries;
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Update statistics
638
+ */
639
+ updateStats() {
640
+ let healthy = 0;
641
+ let unhealthy = 0;
642
+
643
+ for (const health of this.healthChecks.values()) {
644
+ if (health.status === 'healthy') healthy++;
645
+ else if (health.status === 'unhealthy') unhealthy++;
646
+ }
647
+
648
+ this.stats.healthyUrls = healthy;
649
+ this.stats.unhealthyUrls = unhealthy;
650
+ this.stats.lastUpdated = Date.now();
651
+ }
652
+
653
+ /**
654
+ * Get health summary
655
+ * @returns {Object} Health summary
656
+ */
657
+ getHealthSummary() {
658
+ const summary = {
659
+ totalUrls: this.webhookUrls.size,
660
+ healthyUrls: 0,
661
+ unhealthyUrls: 0,
662
+ unknownUrls: 0,
663
+ details: {}
664
+ };
665
+
666
+ for (const [url, health] of this.healthChecks) {
667
+ summary.details[url] = {
668
+ status: health.status,
669
+ consecutiveFailures: health.consecutiveFailures,
670
+ averageResponseTime: health.averageResponseTime,
671
+ lastCheck: health.lastCheck,
672
+ lastError: health.lastError
673
+ };
674
+
675
+ summary[health.status + 'Urls']++;
676
+ }
677
+
678
+ return summary;
679
+ }
680
+
681
+ /**
682
+ * Generate unique event ID
683
+ * @returns {string} Event ID
684
+ */
685
+ generateEventId() {
686
+ return crypto.randomUUID();
687
+ }
688
+
689
+ /**
690
+ * Get comprehensive statistics
691
+ * @returns {Object} Statistics object
692
+ */
693
+ getStats() {
694
+ return Object.assign({}, this.stats, {
695
+ queueSize: this.queue.length,
696
+ registeredUrls: this.webhookUrls.size,
697
+ failedUrlsCount: this.failedUrls.size,
698
+ processing: this.processing,
699
+ retryManagerStats: this.retryManager.getStats()
700
+ });
701
+ }
702
+
703
+ /**
704
+ * Get failed URLs information
705
+ * @returns {Object} Failed URLs mapping
706
+ */
707
+ getFailedUrls() {
708
+ return Object.fromEntries(this.failedUrls);
709
+ }
710
+
711
+ /**
712
+ * Clear failed URLs tracking
713
+ * @param {string} url - Specific URL to clear, or null for all
714
+ */
715
+ clearFailedUrls(url = null) {
716
+ if (url) {
717
+ this.failedUrls.delete(url);
718
+ } else {
719
+ this.failedUrls.clear();
720
+ }
721
+ this.emit('failedUrlsCleared', url);
722
+ }
723
+
724
+ /**
725
+ * Cleanup resources
726
+ */
727
+ destroy() {
728
+ // Stop timers
729
+ this.stopProcessing();
730
+
731
+ // Clear data
732
+ this.queue = [];
733
+ this.webhookUrls.clear();
734
+ this.healthChecks.clear();
735
+ this.responseTimes.clear();
736
+ this.failedUrls.clear();
737
+
738
+ // Remove event listeners
739
+ this.removeAllListeners();
740
+
741
+ this.emit('destroyed');
742
+ }
743
+ }
744
+
745
+ export default WebhookDispatcher;