@fkom13/mcp-sftp-orchestrator 6.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.
package/queue.js ADDED
@@ -0,0 +1,377 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import config from './config.js';
5
+
6
+ const QUEUE_FILE = path.join(config.dataDir, 'queue.json');
7
+ const QUEUE_BACKUP = path.join(config.dataDir, 'queue.backup.json');
8
+ const SAVE_INTERVAL = 5000; // Sauvegarder toutes les 5 secondes
9
+ const MAX_QUEUE_SIZE = 1000; // Limite de tâches en mémoire
10
+
11
+ const jobQueue = {};
12
+ const logHistory = [];
13
+ const MAX_LOGS = 500;
14
+
15
+ let saveTimer = null;
16
+ let isDirty = false;
17
+
18
+ // Charger la queue au démarrage
19
+ async function loadQueue() {
20
+ try {
21
+ const data = await fs.readFile(QUEUE_FILE, 'utf-8');
22
+ const savedQueue = JSON.parse(data);
23
+
24
+ // Restaurer les tâches non terminées
25
+ for (const [id, job] of Object.entries(savedQueue)) {
26
+ // Convertir les dates string en objets Date
27
+ if (job.createdAt) job.createdAt = new Date(job.createdAt);
28
+ if (job.updatedAt) job.updatedAt = new Date(job.updatedAt);
29
+ if (job.reminderAt) job.reminderAt = new Date(job.reminderAt);
30
+
31
+ // Marquer les tâches running comme crashed
32
+ if (job.status === 'running') {
33
+ job.status = 'crashed';
34
+ job.crashedAt = new Date();
35
+ job.canRetry = true;
36
+ log('warn', `Tâche ${id} marquée comme crashed (reprise après redémarrage)`);
37
+ }
38
+
39
+ jobQueue[id] = job;
40
+ }
41
+
42
+ log('info', `${Object.keys(jobQueue).length} tâches restaurées depuis la sauvegarde`);
43
+ } catch (err) {
44
+ if (err.code !== 'ENOENT') {
45
+ log('error', `Erreur lors du chargement de la queue: ${err.message}`);
46
+
47
+ // Essayer de charger la sauvegarde de secours
48
+ try {
49
+ const backupData = await fs.readFile(QUEUE_BACKUP, 'utf-8');
50
+ const backupQueue = JSON.parse(backupData);
51
+ Object.assign(jobQueue, backupQueue);
52
+ log('info', 'Queue restaurée depuis la sauvegarde de secours');
53
+ } catch (backupErr) {
54
+ log('warn', 'Aucune sauvegarde de queue trouvée, démarrage avec une queue vide');
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ let isSaving = false;
61
+
62
+ // Sauvegarder la queue
63
+ async function saveQueue() {
64
+ if (!isDirty || isSaving) return;
65
+
66
+ isSaving = true;
67
+ try {
68
+ // Créer une sauvegarde de l'ancienne queue
69
+ try {
70
+ await fs.copyFile(QUEUE_FILE, QUEUE_BACKUP);
71
+ } catch (e) {
72
+ // Ignorer si le fichier n'existe pas encore
73
+ }
74
+
75
+ // Filtrer les tâches terminées anciennes (garder 24h)
76
+ const now = Date.now();
77
+ const filteredQueue = {};
78
+
79
+ for (const [id, job] of Object.entries(jobQueue)) {
80
+ const age = now - new Date(job.createdAt).getTime();
81
+ const isRecent = age < 86400000; // 24 heures
82
+ const isActive = ['pending', 'running', 'crashed'].includes(job.status);
83
+
84
+ if (isActive || isRecent) {
85
+ filteredQueue[id] = job;
86
+ }
87
+ }
88
+
89
+ // Sauvegarder la queue filtrée
90
+ await fs.writeFile(
91
+ QUEUE_FILE,
92
+ JSON.stringify(filteredQueue, null, 2)
93
+ );
94
+
95
+ isDirty = false;
96
+ log('debug', `Queue sauvegardée (${Object.keys(filteredQueue).length} tâches)`);
97
+ } catch (err) {
98
+ log('error', `Erreur lors de la sauvegarde de la queue: ${err.message}`);
99
+ } finally {
100
+ isSaving = false;
101
+ }
102
+ }
103
+
104
+ // Démarrer la sauvegarde automatique
105
+ function startAutoSave() {
106
+ if (saveTimer) clearInterval(saveTimer);
107
+
108
+ saveTimer = setInterval(() => {
109
+ saveQueue();
110
+ }, SAVE_INTERVAL);
111
+ }
112
+
113
+ // Arrêter la sauvegarde automatique
114
+ function stopAutoSave() {
115
+ if (saveTimer) {
116
+ clearInterval(saveTimer);
117
+ saveTimer = null;
118
+ }
119
+ }
120
+
121
+ function log(level, message) {
122
+ const logEntry = { level, message, timestamp: new Date().toISOString() };
123
+ logHistory.push(logEntry);
124
+ if (logHistory.length > MAX_LOGS) {
125
+ logHistory.shift();
126
+ }
127
+
128
+ // Afficher les logs importants
129
+ if (['error', 'warn', 'info'].includes(level)) {
130
+ const prefix = {
131
+ error: '[❌ ERROR]',
132
+ warn: '[⚠️ WARN]',
133
+ info: '[ℹ️ INFO]',
134
+ debug: '[🔧 DEBUG]'
135
+ }[level] || `[${level.toUpperCase()}]`;
136
+
137
+ console.log(`${prefix} ${new Date().toISOString().split('T')[1].split('.')[0]} - ${message}`);
138
+ }
139
+ }
140
+
141
+ function addJob(details) {
142
+ // Vérifier la taille de la queue
143
+ if (Object.keys(jobQueue).length >= MAX_QUEUE_SIZE) {
144
+ // Nettoyer les vieilles tâches terminées
145
+ cleanOldJobs();
146
+
147
+ if (Object.keys(jobQueue).length >= MAX_QUEUE_SIZE) {
148
+ throw new Error(`Queue pleine (${MAX_QUEUE_SIZE} tâches max)`);
149
+ }
150
+ }
151
+
152
+ const id = uuidv4().split('-')[0];
153
+ const job = {
154
+ id,
155
+ type: details.type || 'unknown',
156
+ ...details,
157
+ createdAt: new Date(),
158
+ retryCount: 0,
159
+ maxRetries: details.maxRetries || 3
160
+ };
161
+
162
+ if (details.rappel && details.rappel > 0) {
163
+ job.reminderAt = new Date(Date.now() + details.rappel * 1000);
164
+ }
165
+
166
+ jobQueue[id] = job;
167
+ isDirty = true;
168
+ log('info', `Nouvelle tâche ${id} (${job.type}) ajoutée.`);
169
+ return jobQueue[id];
170
+ }
171
+
172
+ function updateJobStatus(id, status, data = {}) {
173
+ if (jobQueue[id]) {
174
+ const oldStatus = jobQueue[id].status;
175
+ jobQueue[id].status = status;
176
+ jobQueue[id].updatedAt = new Date();
177
+
178
+ // Copier toutes les données supplémentaires
179
+ Object.assign(jobQueue[id], data);
180
+
181
+ if (data.error) {
182
+ jobQueue[id].error = data.error;
183
+ jobQueue[id].failedAt = new Date();
184
+ log('error', `Tâche ${id} échouée: ${data.error}`);
185
+ } else if (status === 'completed') {
186
+ jobQueue[id].completedAt = new Date();
187
+ const duration = jobQueue[id].completedAt - jobQueue[id].createdAt;
188
+ jobQueue[id].duration = duration;
189
+ log('info', `Tâche ${id} terminée en ${(duration/1000).toFixed(2)}s`);
190
+ } else {
191
+ log('info', `Tâche ${id}: ${oldStatus} -> ${status}`);
192
+ }
193
+
194
+ isDirty = true;
195
+ }
196
+ }
197
+
198
+ function getJob(id) {
199
+ return jobQueue[id];
200
+ }
201
+
202
+ function getQueue() {
203
+ return jobQueue;
204
+ }
205
+
206
+ function getLogs(filter = {}) {
207
+ if (!filter || Object.keys(filter).length === 0) {
208
+ return logHistory;
209
+ }
210
+
211
+ return logHistory.filter(log => {
212
+ if (filter.level && log.level !== filter.level) return false;
213
+ if (filter.since && new Date(log.timestamp) < new Date(filter.since)) return false;
214
+ if (filter.search && !log.message.toLowerCase().includes(filter.search.toLowerCase())) return false;
215
+ return true;
216
+ });
217
+ }
218
+
219
+ // Nettoyer les vieilles tâches terminées
220
+ function cleanOldJobs() {
221
+ const now = Date.now();
222
+ const toDelete = [];
223
+
224
+ for (const [id, job] of Object.entries(jobQueue)) {
225
+ const age = now - new Date(job.createdAt).getTime();
226
+ const isOld = age > 86400000; // Plus de 24h
227
+ const isCompleted = ['completed', 'failed'].includes(job.status);
228
+
229
+ if (isOld && isCompleted) {
230
+ toDelete.push(id);
231
+ }
232
+ }
233
+
234
+ for (const id of toDelete) {
235
+ delete jobQueue[id];
236
+ }
237
+
238
+ if (toDelete.length > 0) {
239
+ log('info', `${toDelete.length} vieilles tâches supprimées de la queue`);
240
+ isDirty = true;
241
+ }
242
+ }
243
+
244
+ // Réessayer une tâche échouée ou crashed
245
+ async function retryJob(id) {
246
+ const job = jobQueue[id];
247
+ if (!job) {
248
+ throw new Error(`Tâche ${id} introuvable`);
249
+ }
250
+
251
+ if (!['failed', 'crashed'].includes(job.status)) {
252
+ throw new Error(`La tâche ${id} ne peut pas être réessayée (statut: ${job.status})`);
253
+ }
254
+
255
+ if (job.retryCount >= job.maxRetries) {
256
+ throw new Error(`La tâche ${id} a atteint le nombre max de tentatives (${job.maxRetries})`);
257
+ }
258
+
259
+ // Créer une nouvelle tâche basée sur l'ancienne
260
+ const newJob = {
261
+ ...job,
262
+ id: uuidv4().split('-')[0],
263
+ status: 'pending',
264
+ retryCount: (job.retryCount || 0) + 1,
265
+ retriedFrom: id,
266
+ createdAt: new Date(),
267
+ updatedAt: new Date(),
268
+ error: null,
269
+ output: null
270
+ };
271
+
272
+ delete newJob.failedAt;
273
+ delete newJob.crashedAt;
274
+ delete newJob.completedAt;
275
+
276
+ jobQueue[newJob.id] = newJob;
277
+ isDirty = true;
278
+
279
+ log('info', `Tâche ${id} réessayée -> nouvelle tâche ${newJob.id} (tentative ${newJob.retryCount}/${newJob.maxRetries})`);
280
+
281
+ return newJob;
282
+ }
283
+
284
+ // Obtenir les tâches crashées qui peuvent être reprises
285
+ function getCrashedJobs() {
286
+ return Object.values(jobQueue).filter(job =>
287
+ job.status === 'crashed' &&
288
+ job.canRetry &&
289
+ job.retryCount < job.maxRetries
290
+ );
291
+ }
292
+
293
+ // Statistiques de la queue
294
+ function getStats() {
295
+ const stats = {
296
+ total: Object.keys(jobQueue).length,
297
+ byStatus: {},
298
+ byType: {},
299
+ avgDuration: 0,
300
+ successRate: 0
301
+ };
302
+
303
+ let totalDuration = 0;
304
+ let completedCount = 0;
305
+
306
+ for (const job of Object.values(jobQueue)) {
307
+ // Par statut
308
+ stats.byStatus[job.status] = (stats.byStatus[job.status] || 0) + 1;
309
+
310
+ // Par type
311
+ stats.byType[job.type] = (stats.byType[job.type] || 0) + 1;
312
+
313
+ // Durée moyenne
314
+ if (job.duration) {
315
+ totalDuration += job.duration;
316
+ completedCount++;
317
+ }
318
+ }
319
+
320
+ if (completedCount > 0) {
321
+ stats.avgDuration = Math.round(totalDuration / completedCount);
322
+ }
323
+
324
+ const totalFinished = (stats.byStatus.completed || 0) + (stats.byStatus.failed || 0);
325
+ if (totalFinished > 0) {
326
+ stats.successRate = Math.round((stats.byStatus.completed || 0) / totalFinished * 100);
327
+ }
328
+
329
+ return stats;
330
+ }
331
+
332
+ // Initialiser le module
333
+ async function init() {
334
+ await loadQueue();
335
+ startAutoSave();
336
+
337
+ // Nettoyer périodiquement
338
+ setInterval(cleanOldJobs, 3600000); // Toutes les heures
339
+ }
340
+
341
+ // Arrêter proprement
342
+ async function shutdown() {
343
+ log('info', 'Arrêt du gestionnaire de queue...');
344
+ stopAutoSave();
345
+ await saveQueue();
346
+ }
347
+
348
+ // Gérer l'arrêt du processus
349
+ process.on('SIGINT', async () => {
350
+ await shutdown();
351
+ process.exit(0);
352
+ });
353
+
354
+ process.on('SIGTERM', async () => {
355
+ await shutdown();
356
+ process.exit(0);
357
+ });
358
+
359
+ // Démarrer automatiquement
360
+ init().catch(err => {
361
+ console.error('Erreur lors de l\'initialisation de la queue:', err);
362
+ });
363
+
364
+ export default {
365
+ addJob,
366
+ updateJobStatus,
367
+ getJob,
368
+ getQueue,
369
+ getLogs,
370
+ log,
371
+ retryJob,
372
+ getCrashedJobs,
373
+ getStats,
374
+ cleanOldJobs,
375
+ saveQueue,
376
+ shutdown
377
+ };