@172ai/containers-mcp-server 1.12.3 → 1.12.4
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/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +642 -0
- package/dist/server.js.map +1 -1
- package/dist/services/exportService.d.ts +108 -0
- package/dist/services/exportService.d.ts.map +1 -0
- package/dist/services/exportService.js +659 -0
- package/dist/services/exportService.js.map +1 -0
- package/dist/services/importService.d.ts +90 -0
- package/dist/services/importService.d.ts.map +1 -0
- package/dist/services/importService.js +570 -0
- package/dist/services/importService.js.map +1 -0
- package/dist/services/userNotificationManager.d.ts +18 -0
- package/dist/services/userNotificationManager.d.ts.map +1 -1
- package/dist/services/userNotificationManager.js +44 -0
- package/dist/services/userNotificationManager.js.map +1 -1
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { authManager } from '../auth.js';
|
|
2
|
+
import { ErrorHandler, ApiError } from '../utils/errorHandler.js';
|
|
3
|
+
import { EnhancedMCPErrorHandler, MCP_ERROR_CODES, MCPError } from '../utils/enhancedErrorHandler.js';
|
|
4
|
+
import { streamingService } from './streamingService.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generate a unique request ID
|
|
7
|
+
*/
|
|
8
|
+
function generateRequestId() {
|
|
9
|
+
return `mcp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
10
|
+
}
|
|
11
|
+
export class ExportService {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.mcpServer = null;
|
|
14
|
+
// Real-time streaming management
|
|
15
|
+
this.activeStreams = new Map();
|
|
16
|
+
this.progressUpdates = new Map();
|
|
17
|
+
this.streamAnalytics = {
|
|
18
|
+
totalExports: 0,
|
|
19
|
+
activeExports: 0,
|
|
20
|
+
completedExports: 0,
|
|
21
|
+
failedExports: 0,
|
|
22
|
+
averageExportTime: 0,
|
|
23
|
+
notificationsSent: 0,
|
|
24
|
+
errorsEncountered: 0,
|
|
25
|
+
lastActivity: new Date().toISOString()
|
|
26
|
+
};
|
|
27
|
+
this.httpClient = authManager.getHttpClient();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set MCP server reference for progress notifications
|
|
31
|
+
*/
|
|
32
|
+
setMCPServer(server) {
|
|
33
|
+
this.mcpServer = server;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get stream analytics and monitoring information
|
|
37
|
+
*/
|
|
38
|
+
getStreamAnalytics() {
|
|
39
|
+
return {
|
|
40
|
+
...this.streamAnalytics,
|
|
41
|
+
activeStreams: Array.from(this.activeStreams.values())
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get progress update for a specific token
|
|
46
|
+
*/
|
|
47
|
+
getProgressUpdate(progressToken) {
|
|
48
|
+
const update = this.progressUpdates.get(progressToken);
|
|
49
|
+
if (!update)
|
|
50
|
+
return null;
|
|
51
|
+
// Check if expired (clean up stale data)
|
|
52
|
+
if (update.expiresAt && Date.now() > update.expiresAt) {
|
|
53
|
+
this.progressUpdates.delete(progressToken);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return update;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Cancel export monitoring for a specific token
|
|
60
|
+
*/
|
|
61
|
+
async cancelExportMonitoring(progressToken, reason = 'User requested cancellation') {
|
|
62
|
+
const stream = this.activeStreams.get(progressToken);
|
|
63
|
+
if (!stream) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
console.log(`[Export] Cancelling monitoring for export job ${stream.jobId} with token ${progressToken}`);
|
|
67
|
+
try {
|
|
68
|
+
// Update progress with cancellation notice
|
|
69
|
+
await this.storeProgressUpdate(progressToken, stream.lastProgress, 100, `Export monitoring cancelled: ${reason}`);
|
|
70
|
+
// Unsubscribe from stream
|
|
71
|
+
await streamingService.unsubscribeFromStream(stream.streamId);
|
|
72
|
+
// Cleanup
|
|
73
|
+
this.completeStream(progressToken, 'cancelled');
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(`[Export] Error cancelling monitoring for token ${progressToken}:`, error);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Start a container export with real-time streaming progress
|
|
83
|
+
*/
|
|
84
|
+
async exportContainer(params, progressToken) {
|
|
85
|
+
try {
|
|
86
|
+
const { containerId, destinationType, destination, options } = params;
|
|
87
|
+
if (!containerId || !destinationType || !destination) {
|
|
88
|
+
throw ErrorHandler.createValidationError('Container ID, destination type, and destination are required');
|
|
89
|
+
}
|
|
90
|
+
// Initialize progress tracking with streaming notifications
|
|
91
|
+
if (progressToken) {
|
|
92
|
+
console.log(`[Export] Starting export with streaming progress token: ${progressToken}`);
|
|
93
|
+
await this.storeProgressUpdate(progressToken, 0, 100, 'Export initialization started');
|
|
94
|
+
}
|
|
95
|
+
const exportRequest = {
|
|
96
|
+
destinationType,
|
|
97
|
+
destination,
|
|
98
|
+
options: options || {}
|
|
99
|
+
};
|
|
100
|
+
// Start the export via API
|
|
101
|
+
const response = await this.httpClient.post(`/v1/containers/${containerId}/export`, exportRequest);
|
|
102
|
+
if (!response.data) {
|
|
103
|
+
throw ErrorHandler.createServerError('Invalid response from export API');
|
|
104
|
+
}
|
|
105
|
+
const data = response.data;
|
|
106
|
+
const exportResult = {
|
|
107
|
+
jobId: data.jobId,
|
|
108
|
+
containerId: data.containerId || containerId,
|
|
109
|
+
containerName: data.containerName || '',
|
|
110
|
+
destinationType: data.destinationType,
|
|
111
|
+
destination: data.destination || destination,
|
|
112
|
+
status: data.status || 'pending',
|
|
113
|
+
progress: data.progress || {
|
|
114
|
+
stage: 'pending',
|
|
115
|
+
stageNumber: 0,
|
|
116
|
+
totalStages: 7,
|
|
117
|
+
percent: 0,
|
|
118
|
+
message: 'Export queued'
|
|
119
|
+
},
|
|
120
|
+
result: data.result,
|
|
121
|
+
errors: data.errors || [],
|
|
122
|
+
tokenCost: data.tokenCost || 0,
|
|
123
|
+
createdAt: data.createdAt || new Date().toISOString(),
|
|
124
|
+
updatedAt: data.updatedAt || new Date().toISOString(),
|
|
125
|
+
completedAt: data.completedAt,
|
|
126
|
+
estimatedDuration: data.estimatedDuration,
|
|
127
|
+
metadata: data.metadata
|
|
128
|
+
};
|
|
129
|
+
// Set up real-time streaming for progress updates
|
|
130
|
+
if (progressToken) {
|
|
131
|
+
await this.storeProgressUpdate(progressToken, 5, 100, `Export job created - ID: ${exportResult.jobId}`);
|
|
132
|
+
// Create streaming subscription
|
|
133
|
+
await this.setupStreamingMonitoring(exportResult.jobId, containerId, destinationType, exportResult.containerName, progressToken);
|
|
134
|
+
}
|
|
135
|
+
return exportResult;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Use enhanced error handling for export operations
|
|
139
|
+
const context = {
|
|
140
|
+
operation: 'export_container',
|
|
141
|
+
containerId: params.containerId,
|
|
142
|
+
destinationType: params.destinationType,
|
|
143
|
+
exportStage: 'initialization',
|
|
144
|
+
progressToken: progressToken,
|
|
145
|
+
requestId: generateRequestId()
|
|
146
|
+
};
|
|
147
|
+
if (error instanceof ApiError && error.status === 402) {
|
|
148
|
+
throw new MCPError(MCP_ERROR_CODES.API_QUOTA_EXCEEDED, error.message || 'Insufficient tokens for export', context);
|
|
149
|
+
}
|
|
150
|
+
if (error instanceof ApiError && error.status === 400) {
|
|
151
|
+
throw new MCPError(MCP_ERROR_CODES.CONTAINER_STATE_INVALID, error.message || 'Invalid export request', context);
|
|
152
|
+
}
|
|
153
|
+
const enhancedError = EnhancedMCPErrorHandler.processError(error, context);
|
|
154
|
+
throw new ApiError(enhancedError.message, enhancedError.status, enhancedError.code, enhancedError, context.requestId, context.operation);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Set up real-time streaming monitoring for export progress
|
|
159
|
+
*/
|
|
160
|
+
async setupStreamingMonitoring(jobId, containerId, destinationType, containerName, progressToken) {
|
|
161
|
+
try {
|
|
162
|
+
console.log(`[Export] Setting up real-time streaming for export job ${jobId} with token ${progressToken}`);
|
|
163
|
+
// Create export stream
|
|
164
|
+
const streamSession = await streamingService.createStream({
|
|
165
|
+
streamType: 'user-notifications',
|
|
166
|
+
resourceId: jobId,
|
|
167
|
+
eventFilters: [
|
|
168
|
+
'export-started',
|
|
169
|
+
'export-validating',
|
|
170
|
+
'export-building',
|
|
171
|
+
'export-preparing',
|
|
172
|
+
'export-authenticating',
|
|
173
|
+
'export-uploading',
|
|
174
|
+
'export-pushing',
|
|
175
|
+
'export-verifying',
|
|
176
|
+
'export-completed',
|
|
177
|
+
'export-failed'
|
|
178
|
+
],
|
|
179
|
+
ttlMinutes: 120, // 2 hours
|
|
180
|
+
metadata: {
|
|
181
|
+
jobId,
|
|
182
|
+
containerId,
|
|
183
|
+
progressToken,
|
|
184
|
+
destinationType,
|
|
185
|
+
containerName
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// Track active stream
|
|
189
|
+
const activeStream = {
|
|
190
|
+
jobId,
|
|
191
|
+
containerId,
|
|
192
|
+
containerName,
|
|
193
|
+
destinationType,
|
|
194
|
+
progressToken,
|
|
195
|
+
streamId: streamSession.streamId,
|
|
196
|
+
startTime: Date.now(),
|
|
197
|
+
lastProgress: 5,
|
|
198
|
+
status: 'active'
|
|
199
|
+
};
|
|
200
|
+
this.activeStreams.set(progressToken, activeStream);
|
|
201
|
+
this.streamAnalytics.totalExports++;
|
|
202
|
+
this.streamAnalytics.activeExports++;
|
|
203
|
+
// Subscribe to stream events
|
|
204
|
+
await streamingService.subscribeToStream(streamSession.streamId, (event) => {
|
|
205
|
+
this.handleExportStreamEvent(event, progressToken);
|
|
206
|
+
});
|
|
207
|
+
console.log(`[Export] Real-time streaming active for export job ${jobId} (stream: ${streamSession.streamId})`);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
console.error(`[Export] Failed to setup streaming monitoring for job ${jobId}:`, {
|
|
211
|
+
error: error?.message || error,
|
|
212
|
+
stack: error?.stack,
|
|
213
|
+
jobId,
|
|
214
|
+
progressToken
|
|
215
|
+
});
|
|
216
|
+
// Fallback to basic progress tracking if streaming fails
|
|
217
|
+
await this.storeProgressUpdate(progressToken, 5, 100, `Export started - streaming unavailable: ${error?.message || 'Unknown error'}`);
|
|
218
|
+
// Start fallback polling mechanism
|
|
219
|
+
await this.startFallbackProgressPolling(jobId, progressToken);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Start fallback polling mechanism when streaming fails
|
|
224
|
+
*/
|
|
225
|
+
async startFallbackProgressPolling(jobId, progressToken) {
|
|
226
|
+
console.log(`[Export] Starting fallback polling for export job ${jobId} with token ${progressToken}`);
|
|
227
|
+
const stream = this.activeStreams.get(progressToken);
|
|
228
|
+
if (!stream) {
|
|
229
|
+
console.error(`[Export] No stream found for token ${progressToken}`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Start polling every 10 seconds
|
|
233
|
+
const pollInterval = setInterval(async () => {
|
|
234
|
+
try {
|
|
235
|
+
const exportStatus = await this.getExportStatus(jobId);
|
|
236
|
+
if (!this.activeStreams.has(progressToken)) {
|
|
237
|
+
clearInterval(pollInterval);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const currentStream = this.activeStreams.get(progressToken);
|
|
241
|
+
let progress = currentStream.lastProgress;
|
|
242
|
+
let message = `Export ${exportStatus.status}`;
|
|
243
|
+
// Map export status to progress
|
|
244
|
+
switch (exportStatus.status) {
|
|
245
|
+
case 'pending':
|
|
246
|
+
progress = 5;
|
|
247
|
+
message = 'Export queued and waiting to start';
|
|
248
|
+
break;
|
|
249
|
+
case 'validating':
|
|
250
|
+
progress = 10;
|
|
251
|
+
message = exportStatus.progress?.message || 'Validating export configuration';
|
|
252
|
+
break;
|
|
253
|
+
case 'building':
|
|
254
|
+
progress = 30;
|
|
255
|
+
message = exportStatus.progress?.message || 'Building container image';
|
|
256
|
+
break;
|
|
257
|
+
case 'preparing':
|
|
258
|
+
progress = 40;
|
|
259
|
+
message = exportStatus.progress?.message || 'Preparing export';
|
|
260
|
+
break;
|
|
261
|
+
case 'authenticating':
|
|
262
|
+
progress = 50;
|
|
263
|
+
message = exportStatus.progress?.message || 'Authenticating with destination';
|
|
264
|
+
break;
|
|
265
|
+
case 'uploading':
|
|
266
|
+
progress = 70;
|
|
267
|
+
message = exportStatus.progress?.message || 'Uploading files';
|
|
268
|
+
break;
|
|
269
|
+
case 'pushing':
|
|
270
|
+
progress = 80;
|
|
271
|
+
message = exportStatus.progress?.message || 'Pushing to destination';
|
|
272
|
+
break;
|
|
273
|
+
case 'verifying':
|
|
274
|
+
progress = 95;
|
|
275
|
+
message = exportStatus.progress?.message || 'Verifying export';
|
|
276
|
+
break;
|
|
277
|
+
case 'completed':
|
|
278
|
+
progress = 100;
|
|
279
|
+
message = this.getCompletionMessage(exportStatus);
|
|
280
|
+
break;
|
|
281
|
+
case 'failed':
|
|
282
|
+
message = `Export failed: ${exportStatus.errors?.[0] || 'Unknown error'}`;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
// Update progress
|
|
286
|
+
currentStream.lastProgress = progress;
|
|
287
|
+
await this.storeProgressUpdate(progressToken, progress, 100, message, {
|
|
288
|
+
jobId,
|
|
289
|
+
status: exportStatus.status,
|
|
290
|
+
fallbackMode: true
|
|
291
|
+
});
|
|
292
|
+
// Complete if finished
|
|
293
|
+
if (['completed', 'failed', 'cancelled'].includes(exportStatus.status)) {
|
|
294
|
+
const completionTime = Date.now() - currentStream.startTime;
|
|
295
|
+
this.completeStream(progressToken, exportStatus.status === 'completed' ? 'completed' : 'failed', completionTime);
|
|
296
|
+
clearInterval(pollInterval);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.error(`[Export] Error in fallback polling for ${jobId}:`, error);
|
|
301
|
+
}
|
|
302
|
+
}, 10000); // Poll every 10 seconds
|
|
303
|
+
// Auto-cleanup after 30 minutes
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
if (this.activeStreams.has(progressToken)) {
|
|
306
|
+
console.log(`[Export] Auto-cleaning up fallback polling for ${jobId} after 30 minutes`);
|
|
307
|
+
clearInterval(pollInterval);
|
|
308
|
+
this.completeStream(progressToken, 'timeout');
|
|
309
|
+
}
|
|
310
|
+
}, 30 * 60 * 1000);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get completion message based on export result
|
|
314
|
+
*/
|
|
315
|
+
getCompletionMessage(exportStatus) {
|
|
316
|
+
if (exportStatus.result?.imageUrl) {
|
|
317
|
+
return `Export completed! Image pushed to ${exportStatus.result.imageUrl}`;
|
|
318
|
+
}
|
|
319
|
+
if (exportStatus.result?.repositoryUrl) {
|
|
320
|
+
return `Export completed! Repository: ${exportStatus.result.repositoryUrl}`;
|
|
321
|
+
}
|
|
322
|
+
if (exportStatus.result?.downloadUrl) {
|
|
323
|
+
return `Export completed! Download ready (expires ${exportStatus.result.expiresAt})`;
|
|
324
|
+
}
|
|
325
|
+
return 'Export completed successfully!';
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Handle real-time export stream events
|
|
329
|
+
*/
|
|
330
|
+
async handleExportStreamEvent(event, progressToken) {
|
|
331
|
+
try {
|
|
332
|
+
const stream = this.activeStreams.get(progressToken);
|
|
333
|
+
if (!stream) {
|
|
334
|
+
console.warn(`[Export] Received event for unknown progress token: ${progressToken}`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
console.log(`[Export] Processing ${event.eventType} for export job ${stream.jobId}: ${event.data.message || event.data.data?.message || ''}`);
|
|
338
|
+
// Convert stream event to progress update
|
|
339
|
+
const progressUpdate = this.convertStreamEventToProgress(event, progressToken, stream);
|
|
340
|
+
// Store progress update
|
|
341
|
+
await this.storeProgressUpdate(progressToken, progressUpdate.progress, progressUpdate.total, progressUpdate.message, progressUpdate.metadata);
|
|
342
|
+
// Update stream tracking
|
|
343
|
+
stream.lastProgress = progressUpdate.progress;
|
|
344
|
+
// Handle completion events
|
|
345
|
+
if (['export-completed', 'export-failed'].includes(event.eventType)) {
|
|
346
|
+
const completionTime = Date.now() - stream.startTime;
|
|
347
|
+
this.completeStream(progressToken, event.eventType.replace('export-', ''), completionTime);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
console.error(`[Export] Error handling stream event:`, error);
|
|
352
|
+
this.streamAnalytics.errorsEncountered++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Convert stream event to progress update format
|
|
357
|
+
*/
|
|
358
|
+
convertStreamEventToProgress(event, progressToken, stream) {
|
|
359
|
+
const baseProgress = {
|
|
360
|
+
progressToken,
|
|
361
|
+
total: 100,
|
|
362
|
+
timestamp: event.timestamp,
|
|
363
|
+
metadata: {
|
|
364
|
+
eventType: event.eventType,
|
|
365
|
+
jobId: stream.jobId,
|
|
366
|
+
containerId: stream.containerId,
|
|
367
|
+
destinationType: stream.destinationType,
|
|
368
|
+
streamId: stream.streamId,
|
|
369
|
+
...event.data.data
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const eventData = event.data.data || event.data;
|
|
373
|
+
switch (event.eventType) {
|
|
374
|
+
case 'export-started':
|
|
375
|
+
return {
|
|
376
|
+
...baseProgress,
|
|
377
|
+
progress: 5,
|
|
378
|
+
message: eventData.message || 'Export started'
|
|
379
|
+
};
|
|
380
|
+
case 'export-validating':
|
|
381
|
+
return {
|
|
382
|
+
...baseProgress,
|
|
383
|
+
progress: 10,
|
|
384
|
+
message: eventData.message || 'Validating export configuration'
|
|
385
|
+
};
|
|
386
|
+
case 'export-building':
|
|
387
|
+
return {
|
|
388
|
+
...baseProgress,
|
|
389
|
+
progress: 30,
|
|
390
|
+
message: eventData.message || 'Building container image'
|
|
391
|
+
};
|
|
392
|
+
case 'export-preparing':
|
|
393
|
+
return {
|
|
394
|
+
...baseProgress,
|
|
395
|
+
progress: 40,
|
|
396
|
+
message: eventData.message || 'Preparing export files'
|
|
397
|
+
};
|
|
398
|
+
case 'export-authenticating':
|
|
399
|
+
return {
|
|
400
|
+
...baseProgress,
|
|
401
|
+
progress: 50,
|
|
402
|
+
message: eventData.message || 'Authenticating with destination'
|
|
403
|
+
};
|
|
404
|
+
case 'export-uploading':
|
|
405
|
+
return {
|
|
406
|
+
...baseProgress,
|
|
407
|
+
progress: 70,
|
|
408
|
+
message: eventData.message || 'Uploading files to destination'
|
|
409
|
+
};
|
|
410
|
+
case 'export-pushing':
|
|
411
|
+
return {
|
|
412
|
+
...baseProgress,
|
|
413
|
+
progress: 80,
|
|
414
|
+
message: eventData.message || 'Pushing to registry/repository'
|
|
415
|
+
};
|
|
416
|
+
case 'export-verifying':
|
|
417
|
+
return {
|
|
418
|
+
...baseProgress,
|
|
419
|
+
progress: 95,
|
|
420
|
+
message: eventData.message || 'Verifying export completion'
|
|
421
|
+
};
|
|
422
|
+
case 'export-completed':
|
|
423
|
+
return {
|
|
424
|
+
...baseProgress,
|
|
425
|
+
progress: 100,
|
|
426
|
+
message: eventData.message || `Export completed successfully!`
|
|
427
|
+
};
|
|
428
|
+
case 'export-failed':
|
|
429
|
+
return {
|
|
430
|
+
...baseProgress,
|
|
431
|
+
progress: Math.max(stream.lastProgress, 0),
|
|
432
|
+
message: eventData.message || eventData.error || 'Export failed'
|
|
433
|
+
};
|
|
434
|
+
default:
|
|
435
|
+
return {
|
|
436
|
+
...baseProgress,
|
|
437
|
+
progress: Math.max(stream.lastProgress, 0),
|
|
438
|
+
message: eventData.message || `Unknown event: ${event.eventType}`
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Store progress update with expiration
|
|
444
|
+
*/
|
|
445
|
+
async storeProgressUpdate(progressToken, progress, total = 100, message, metadata) {
|
|
446
|
+
if (!progressToken)
|
|
447
|
+
return;
|
|
448
|
+
try {
|
|
449
|
+
const notificationData = {
|
|
450
|
+
progressToken,
|
|
451
|
+
progress,
|
|
452
|
+
total,
|
|
453
|
+
message,
|
|
454
|
+
timestamp: new Date().toISOString(),
|
|
455
|
+
expiresAt: Date.now() + (30 * 60 * 1000), // 30-minute expiration
|
|
456
|
+
...(metadata && { metadata })
|
|
457
|
+
};
|
|
458
|
+
this.progressUpdates.set(progressToken, notificationData);
|
|
459
|
+
this.streamAnalytics.notificationsSent++;
|
|
460
|
+
this.streamAnalytics.lastActivity = new Date().toISOString();
|
|
461
|
+
console.log(`[Export] Progress stored: ${progress}/${total} - ${message} (token: ${progressToken})`);
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
console.error(`[Export] Failed to store progress update:`, error);
|
|
465
|
+
this.streamAnalytics.errorsEncountered++;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Complete stream monitoring and update analytics
|
|
470
|
+
*/
|
|
471
|
+
completeStream(progressToken, reason, completionTime) {
|
|
472
|
+
const stream = this.activeStreams.get(progressToken);
|
|
473
|
+
if (!stream)
|
|
474
|
+
return;
|
|
475
|
+
// Unsubscribe from stream
|
|
476
|
+
streamingService.unsubscribeFromStream(stream.streamId).catch(error => {
|
|
477
|
+
console.error(`[Export] Error unsubscribing from stream ${stream.streamId}:`, error);
|
|
478
|
+
});
|
|
479
|
+
// Update analytics
|
|
480
|
+
this.streamAnalytics.activeExports = Math.max(0, this.streamAnalytics.activeExports - 1);
|
|
481
|
+
if (reason === 'completed') {
|
|
482
|
+
this.streamAnalytics.completedExports++;
|
|
483
|
+
if (completionTime) {
|
|
484
|
+
const currentAvg = this.streamAnalytics.averageExportTime;
|
|
485
|
+
const totalCompleted = this.streamAnalytics.completedExports;
|
|
486
|
+
this.streamAnalytics.averageExportTime =
|
|
487
|
+
((currentAvg * (totalCompleted - 1)) + completionTime) / totalCompleted;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (reason === 'failed') {
|
|
491
|
+
this.streamAnalytics.failedExports++;
|
|
492
|
+
}
|
|
493
|
+
// Clean up
|
|
494
|
+
this.activeStreams.delete(progressToken);
|
|
495
|
+
console.log(`[Export] Stream monitoring completed for ${stream.jobId} (${reason})`);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get export job status
|
|
499
|
+
*/
|
|
500
|
+
async getExportStatus(jobId) {
|
|
501
|
+
try {
|
|
502
|
+
if (!jobId) {
|
|
503
|
+
throw ErrorHandler.createValidationError('Job ID is required');
|
|
504
|
+
}
|
|
505
|
+
const response = await this.httpClient.get(`/v1/containers/exports/${jobId}`);
|
|
506
|
+
if (!response.data || !response.data.export) {
|
|
507
|
+
throw ErrorHandler.createNotFoundError(`Export job ${jobId} not found`);
|
|
508
|
+
}
|
|
509
|
+
const job = response.data.export;
|
|
510
|
+
return {
|
|
511
|
+
jobId: job.jobId,
|
|
512
|
+
containerId: job.containerId,
|
|
513
|
+
containerName: job.containerName,
|
|
514
|
+
destinationType: job.destinationType,
|
|
515
|
+
destination: job.destination,
|
|
516
|
+
status: job.status,
|
|
517
|
+
progress: job.progress,
|
|
518
|
+
result: job.result,
|
|
519
|
+
errors: job.errors || [],
|
|
520
|
+
tokenCost: job.tokenCost,
|
|
521
|
+
createdAt: job.createdAt,
|
|
522
|
+
updatedAt: job.updatedAt,
|
|
523
|
+
completedAt: job.completedAt,
|
|
524
|
+
estimatedDuration: job.estimatedDuration,
|
|
525
|
+
metadata: job.metadata
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
530
|
+
throw ErrorHandler.createNotFoundError(`Export job ${jobId} not found`);
|
|
531
|
+
}
|
|
532
|
+
throw ErrorHandler.processError(error, 'ExportService.getExportStatus');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* List export jobs
|
|
537
|
+
*/
|
|
538
|
+
async listExportJobs(params = {}) {
|
|
539
|
+
try {
|
|
540
|
+
const queryParams = new URLSearchParams();
|
|
541
|
+
if (params.containerId)
|
|
542
|
+
queryParams.append('containerId', params.containerId);
|
|
543
|
+
if (params.status)
|
|
544
|
+
queryParams.append('status', params.status);
|
|
545
|
+
if (params.limit)
|
|
546
|
+
queryParams.append('limit', params.limit.toString());
|
|
547
|
+
if (params.offset)
|
|
548
|
+
queryParams.append('offset', params.offset.toString());
|
|
549
|
+
let endpoint = '/v1/users/me/exports';
|
|
550
|
+
if (params.containerId) {
|
|
551
|
+
endpoint = `/v1/containers/${params.containerId}/exports`;
|
|
552
|
+
}
|
|
553
|
+
const response = await this.httpClient.get(`${endpoint}?${queryParams.toString()}`);
|
|
554
|
+
if (!response.data) {
|
|
555
|
+
throw ErrorHandler.createServerError('Invalid response format from exports list API');
|
|
556
|
+
}
|
|
557
|
+
const data = response.data;
|
|
558
|
+
return {
|
|
559
|
+
exports: (data.exports || []).map((job) => ({
|
|
560
|
+
jobId: job.jobId,
|
|
561
|
+
containerId: job.containerId,
|
|
562
|
+
containerName: job.containerName,
|
|
563
|
+
destinationType: job.destinationType,
|
|
564
|
+
destination: job.destination,
|
|
565
|
+
status: job.status,
|
|
566
|
+
progress: job.progress,
|
|
567
|
+
result: job.result,
|
|
568
|
+
errors: job.errors || [],
|
|
569
|
+
tokenCost: job.tokenCost,
|
|
570
|
+
createdAt: job.createdAt,
|
|
571
|
+
updatedAt: job.updatedAt,
|
|
572
|
+
completedAt: job.completedAt,
|
|
573
|
+
estimatedDuration: job.estimatedDuration,
|
|
574
|
+
metadata: job.metadata
|
|
575
|
+
})),
|
|
576
|
+
pagination: data.pagination || {
|
|
577
|
+
limit: params.limit || 50,
|
|
578
|
+
offset: params.offset || 0,
|
|
579
|
+
total: data.exports?.length || 0,
|
|
580
|
+
hasMore: false
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
throw ErrorHandler.processError(error, 'ExportService.listExportJobs');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Get download URL for archive exports
|
|
590
|
+
*/
|
|
591
|
+
async getExportDownloadUrl(jobId) {
|
|
592
|
+
try {
|
|
593
|
+
if (!jobId) {
|
|
594
|
+
throw ErrorHandler.createValidationError('Job ID is required');
|
|
595
|
+
}
|
|
596
|
+
const response = await this.httpClient.get(`/v1/containers/exports/${jobId}/download`);
|
|
597
|
+
if (!response.data) {
|
|
598
|
+
throw ErrorHandler.createServerError('Invalid response from download URL API');
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
downloadUrl: response.data.downloadUrl,
|
|
602
|
+
expiresAt: response.data.expiresAt,
|
|
603
|
+
format: response.data.format,
|
|
604
|
+
size: response.data.size
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
609
|
+
throw ErrorHandler.createNotFoundError(`Export job ${jobId} not found or download not available`);
|
|
610
|
+
}
|
|
611
|
+
if (error instanceof ApiError && error.status === 410) {
|
|
612
|
+
throw ErrorHandler.createServerError('Download URL has expired');
|
|
613
|
+
}
|
|
614
|
+
throw ErrorHandler.processError(error, 'ExportService.getExportDownloadUrl');
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Cancel an export job
|
|
619
|
+
*/
|
|
620
|
+
async cancelExportJob(jobId) {
|
|
621
|
+
try {
|
|
622
|
+
if (!jobId) {
|
|
623
|
+
throw ErrorHandler.createValidationError('Job ID is required');
|
|
624
|
+
}
|
|
625
|
+
const response = await this.httpClient.delete(`/v1/containers/exports/${jobId}`);
|
|
626
|
+
if (!response.data || !response.data.export) {
|
|
627
|
+
throw ErrorHandler.createServerError('Invalid response from cancel export API');
|
|
628
|
+
}
|
|
629
|
+
const job = response.data.export;
|
|
630
|
+
return {
|
|
631
|
+
jobId: job.jobId,
|
|
632
|
+
containerId: job.containerId,
|
|
633
|
+
containerName: job.containerName,
|
|
634
|
+
destinationType: job.destinationType,
|
|
635
|
+
destination: job.destination,
|
|
636
|
+
status: job.status,
|
|
637
|
+
progress: job.progress,
|
|
638
|
+
result: job.result,
|
|
639
|
+
errors: job.errors || [],
|
|
640
|
+
tokenCost: job.tokenCost,
|
|
641
|
+
createdAt: job.createdAt,
|
|
642
|
+
updatedAt: job.updatedAt,
|
|
643
|
+
completedAt: job.completedAt
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
648
|
+
throw ErrorHandler.createNotFoundError(`Export job ${jobId} not found`);
|
|
649
|
+
}
|
|
650
|
+
if (error instanceof ApiError && error.status === 400) {
|
|
651
|
+
throw ErrorHandler.createValidationError('Cannot cancel export in current state');
|
|
652
|
+
}
|
|
653
|
+
throw ErrorHandler.processError(error, 'ExportService.cancelExportJob');
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Export singleton instance
|
|
658
|
+
export const exportService = new ExportService();
|
|
659
|
+
//# sourceMappingURL=exportService.js.map
|