@kadi.build/file-sharing 1.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.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * DownloadMonitor - Transfer monitoring for @kadi.build/file-sharing
3
+ *
4
+ * Tracks active downloads, monitors progress, records completion statistics,
5
+ * and provides real-time download information.
6
+ *
7
+ * Migrated from src/downloadMonitor.js
8
+ */
9
+
10
+ import { EventEmitter } from 'events';
11
+
12
+ export class DownloadMonitor extends EventEmitter {
13
+ constructor() {
14
+ super();
15
+ this.activeDownloads = new Map();
16
+ this.completedDownloads = [];
17
+ this.stats = {
18
+ totalBytes: 0,
19
+ totalDownloads: 0,
20
+ activeCount: 0,
21
+ failedCount: 0,
22
+ averageSpeed: 0,
23
+ peakConcurrent: 0
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Register a new download being tracked
29
+ * @param {string} id - Unique download identifier
30
+ * @param {object} metadata - Download metadata
31
+ * @returns {object} The download record
32
+ */
33
+ startDownload(id, metadata = {}) {
34
+ const download = {
35
+ id,
36
+ file: metadata.file || 'unknown',
37
+ totalSize: metadata.totalSize || metadata.size || 0,
38
+ clientIp: metadata.clientIp || 'unknown',
39
+ bytesTransferred: 0,
40
+ progress: 0,
41
+ speed: 0,
42
+ startTime: Date.now(),
43
+ status: 'active',
44
+ ...metadata
45
+ };
46
+
47
+ this.activeDownloads.set(id, download);
48
+ this.stats.activeCount = this.activeDownloads.size;
49
+
50
+ if (this.stats.activeCount > this.stats.peakConcurrent) {
51
+ this.stats.peakConcurrent = this.stats.activeCount;
52
+ }
53
+
54
+ this.emit('download:start', download);
55
+ return download;
56
+ }
57
+
58
+ /**
59
+ * Update download progress
60
+ * @param {string} id - Download identifier
61
+ * @param {number} bytesTransferred - Bytes transferred so far
62
+ */
63
+ updateProgress(id, bytesTransferred) {
64
+ const download = this.activeDownloads.get(id);
65
+ if (!download) return;
66
+
67
+ const now = Date.now();
68
+ const elapsed = (now - download.startTime) / 1000; // seconds
69
+
70
+ download.bytesTransferred = bytesTransferred;
71
+ download.progress = download.totalSize
72
+ ? (bytesTransferred / download.totalSize) * 100
73
+ : 0;
74
+ download.speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
75
+
76
+ this.emit('download:progress', { ...download });
77
+ }
78
+
79
+ /**
80
+ * Mark a download as completed
81
+ * @param {string} id - Download identifier
82
+ */
83
+ completeDownload(id) {
84
+ const download = this.activeDownloads.get(id);
85
+ if (!download) return;
86
+
87
+ download.status = 'completed';
88
+ download.endTime = Date.now();
89
+ download.duration = download.endTime - download.startTime;
90
+ download.progress = 100;
91
+
92
+ this.activeDownloads.delete(id);
93
+ this.completedDownloads.push(download);
94
+
95
+ this.stats.activeCount = this.activeDownloads.size;
96
+ this.stats.totalDownloads++;
97
+ this.stats.totalBytes += download.bytesTransferred;
98
+ this._updateAverageSpeed();
99
+
100
+ this.emit('download:complete', download);
101
+ }
102
+
103
+ /**
104
+ * Mark a download as failed
105
+ * @param {string} id - Download identifier
106
+ * @param {Error} error - The error that caused the failure
107
+ */
108
+ failDownload(id, error) {
109
+ const download = this.activeDownloads.get(id);
110
+ if (!download) return;
111
+
112
+ download.status = 'failed';
113
+ download.error = error.message || String(error);
114
+ download.endTime = Date.now();
115
+ download.duration = download.endTime - download.startTime;
116
+
117
+ this.activeDownloads.delete(id);
118
+ this.completedDownloads.push(download);
119
+
120
+ this.stats.activeCount = this.activeDownloads.size;
121
+ this.stats.failedCount++;
122
+
123
+ this.emit('download:error', { download, error });
124
+ }
125
+
126
+ /**
127
+ * Cancel an active download
128
+ * @param {string} id - Download identifier
129
+ */
130
+ cancelDownload(id) {
131
+ const download = this.activeDownloads.get(id);
132
+ if (!download) return;
133
+
134
+ download.status = 'cancelled';
135
+ download.endTime = Date.now();
136
+ download.duration = download.endTime - download.startTime;
137
+
138
+ this.activeDownloads.delete(id);
139
+ this.completedDownloads.push(download);
140
+ this.stats.activeCount = this.activeDownloads.size;
141
+
142
+ this.emit('download:cancel', download);
143
+ }
144
+
145
+ /**
146
+ * Get current download statistics
147
+ * @returns {object} Download stats
148
+ */
149
+ getStats() {
150
+ return {
151
+ ...this.stats,
152
+ activeDownloads: this.getActiveDownloads()
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Get all active downloads
158
+ * @returns {Array} Active download records
159
+ */
160
+ getActiveDownloads() {
161
+ return Array.from(this.activeDownloads.values());
162
+ }
163
+
164
+ /**
165
+ * Get completed downloads
166
+ * @param {number} limit - Max number of records to return
167
+ * @returns {Array} Completed download records
168
+ */
169
+ getCompletedDownloads(limit = 100) {
170
+ return this.completedDownloads.slice(-limit);
171
+ }
172
+
173
+ /**
174
+ * Clear completed download history
175
+ */
176
+ clearHistory() {
177
+ this.completedDownloads = [];
178
+ }
179
+
180
+ /**
181
+ * Update the running average speed
182
+ * @private
183
+ */
184
+ _updateAverageSpeed() {
185
+ const completed = this.completedDownloads.filter(d => d.status === 'completed' && d.duration > 0);
186
+ if (completed.length === 0) {
187
+ this.stats.averageSpeed = 0;
188
+ return;
189
+ }
190
+
191
+ const totalSpeed = completed.reduce((sum, d) => {
192
+ return sum + (d.bytesTransferred / (d.duration / 1000));
193
+ }, 0);
194
+
195
+ this.stats.averageSpeed = totalSpeed / completed.length;
196
+ }
197
+ }
198
+
199
+ export default DownloadMonitor;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * EventNotifier - Webhook and event notification for @kadi.build/file-sharing
3
+ *
4
+ * Sends notifications to registered webhooks when events occur.
5
+ * Supports filtering by event type, retry logic, and multiple notification channels.
6
+ *
7
+ * Migrated from src/eventNotifier.js
8
+ */
9
+
10
+ import { EventEmitter } from 'events';
11
+
12
+ export class EventNotifier extends EventEmitter {
13
+ constructor(config = {}) {
14
+ super();
15
+
16
+ this.config = {
17
+ maxRetries: 3,
18
+ retryDelay: 1000,
19
+ timeout: 5000,
20
+ ...config
21
+ };
22
+
23
+ this.webhooks = new Map();
24
+ this._notificationHistory = [];
25
+ }
26
+
27
+ /**
28
+ * Register a webhook for event notifications
29
+ * @param {string} url - Webhook URL to send notifications to
30
+ * @param {string[]} events - Array of event names to subscribe to (empty = all events)
31
+ * @param {object} options - Additional options (headers, etc.)
32
+ */
33
+ addWebhook(url, events = [], options = {}) {
34
+ this.webhooks.set(url, {
35
+ url,
36
+ events: events.length > 0 ? new Set(events) : null, // null = all events
37
+ headers: options.headers || {},
38
+ active: true,
39
+ failCount: 0,
40
+ lastNotified: null
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Remove a registered webhook
46
+ * @param {string} url - Webhook URL to remove
47
+ */
48
+ removeWebhook(url) {
49
+ this.webhooks.delete(url);
50
+ }
51
+
52
+ /**
53
+ * Send a notification for an event
54
+ * @param {string} event - Event name
55
+ * @param {object} data - Event data payload
56
+ */
57
+ async notify(event, data = {}) {
58
+ const payload = {
59
+ event,
60
+ timestamp: new Date().toISOString(),
61
+ data
62
+ };
63
+
64
+ const promises = [];
65
+
66
+ for (const [url, webhook] of this.webhooks) {
67
+ if (!webhook.active) continue;
68
+
69
+ // Check event filter
70
+ if (webhook.events && !webhook.events.has(event)) continue;
71
+
72
+ promises.push(this._sendNotification(url, webhook, payload));
73
+ }
74
+
75
+ if (promises.length > 0) {
76
+ await Promise.allSettled(promises);
77
+ }
78
+
79
+ // Record in history
80
+ this._notificationHistory.push({
81
+ event,
82
+ timestamp: payload.timestamp,
83
+ recipientCount: promises.length
84
+ });
85
+
86
+ // Keep history bounded
87
+ if (this._notificationHistory.length > 1000) {
88
+ this._notificationHistory = this._notificationHistory.slice(-500);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Get list of registered webhooks
94
+ * @returns {Array} Webhook configurations
95
+ */
96
+ getWebhooks() {
97
+ return Array.from(this.webhooks.values()).map(w => ({
98
+ url: w.url,
99
+ events: w.events ? Array.from(w.events) : ['*'],
100
+ active: w.active,
101
+ failCount: w.failCount,
102
+ lastNotified: w.lastNotified
103
+ }));
104
+ }
105
+
106
+ /**
107
+ * Get notification history
108
+ * @param {number} limit - Max records to return
109
+ * @returns {Array}
110
+ */
111
+ getHistory(limit = 50) {
112
+ return this._notificationHistory.slice(-limit);
113
+ }
114
+
115
+ /**
116
+ * Send notification to a single webhook with retry logic
117
+ * @param {string} url - Webhook URL
118
+ * @param {object} webhook - Webhook config
119
+ * @param {object} payload - Notification payload
120
+ * @private
121
+ */
122
+ async _sendNotification(url, webhook, payload) {
123
+ let lastError;
124
+
125
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
126
+ try {
127
+ const controller = new AbortController();
128
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
129
+
130
+ const response = await fetch(url, {
131
+ method: 'POST',
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ 'User-Agent': 'KadiFileSharing/1.0',
135
+ ...webhook.headers
136
+ },
137
+ body: JSON.stringify(payload),
138
+ signal: controller.signal
139
+ });
140
+
141
+ clearTimeout(timeoutId);
142
+
143
+ if (response.ok) {
144
+ webhook.failCount = 0;
145
+ webhook.lastNotified = new Date().toISOString();
146
+ this.emit('notification:sent', { url, event: payload.event });
147
+ return;
148
+ }
149
+
150
+ lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
151
+ } catch (error) {
152
+ lastError = error;
153
+ }
154
+
155
+ // Wait before retry
156
+ if (attempt < this.config.maxRetries) {
157
+ await new Promise(resolve =>
158
+ setTimeout(resolve, this.config.retryDelay * (attempt + 1))
159
+ );
160
+ }
161
+ }
162
+
163
+ // All retries failed
164
+ webhook.failCount++;
165
+ if (webhook.failCount >= 10) {
166
+ webhook.active = false;
167
+ this.emit('webhook:disabled', { url, reason: 'Too many failures' });
168
+ }
169
+
170
+ this.emit('notification:failed', {
171
+ url,
172
+ event: payload.event,
173
+ error: lastError?.message || 'Unknown error'
174
+ });
175
+ }
176
+ }
177
+
178
+ export default EventNotifier;