solver-sdk
Version:
SDK for WorkAI API - AI-powered code analysis with WorkCoins billing system
362 lines • 18.2 kB
JavaScript
;
/**
* 🔄 Delta-Chunking Manager
* Менеджер для координации delta-chunking операций
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeltaChunkingManager = void 0;
/**
* Менеджер для координации всех delta-chunking операций
*/
class DeltaChunkingManager {
constructor(deltaApi, utils, options, logger) {
this.deltaApi = deltaApi;
this.utils = utils;
this.options = options;
this.logger = logger;
}
/**
* Проверка активности delta-chunking
*/
get isEnabled() {
return !!this.deltaApi && !!this.options.deltaChunking?.enabled;
}
/**
* 📤 Синхронизация готовых зашифрованных чанков
*
* ✅ ПРАВИЛЬНАЯ АРХИТЕКТУРА:
* Client Extension передает готовые чанки и rootHash
* Backend SDK отправляет их на сервер через HTTP API
*
* @param projectId ID проекта
* @param encryptedChunks Готовые зашифрованные чанки от клиента
* @param rootHash Готовый clientRootHash от клиента
*/
async syncEncryptedChunks(projectId, encryptedChunks, rootHash, options = {}) {
this.ensureEnabled();
const startTime = Date.now();
try {
this.logger.log(`📤 Начинаю синхронизацию ${encryptedChunks.length} готовых чанков для проекта ${projectId}`);
// 📁 Подсчитываем количество уникальных файлов
const uniqueFiles = new Set(encryptedChunks
.map((chunk) => chunk.obfuscatedPath)
.filter((path) => !!path));
const totalFiles = uniqueFiles.size;
this.logger.log(`📁 Обнаружено ${totalFiles} уникальных файлов в ${encryptedChunks.length} чанках`);
// 1. Инициализация синхронизации
const initResult = await this.deltaApi.initSync(projectId, {
clientRootHash: rootHash,
metadata: {
totalFiles, // 📁 Передаем количество файлов для user-friendly прогресса
expectedChunks: encryptedChunks.length,
}
});
// ✅ ИСПРАВЛЕНО: Проверяем нужна ли синхронизация
if (!initResult.needsSync) {
this.logger.log(`✅ Синхронизация не требуется - проект актуален (hash: ${initResult.lastSyncHash})`);
return {
success: true,
projectId,
processedChunks: 0,
totalChunks: encryptedChunks.length,
duration: Date.now() - startTime,
sessionId: undefined,
details: {
newFiles: 0,
changedFiles: 0,
deletedFiles: 0
}
};
}
if (!initResult.sessionId) {
throw new Error('Backend error: sessionId missing when sync is needed');
}
this.logger.log(`🔄 Сессия инициализирована: ${initResult.sessionId}`);
// 2. Отправка готовых зашифрованных чанков батчами
const batchSize = options.batchSize || 50;
let processedTotal = 0;
const errors = [];
for (let i = 0; i < encryptedChunks.length; i += batchSize) {
const batch = encryptedChunks.slice(i, i + batchSize);
const batchNumber = Math.floor(i / batchSize) + 1;
const totalBatches = Math.ceil(encryptedChunks.length / batchSize);
try {
this.logger.log(`📦 Отправка батча ${batchNumber}/${totalBatches} (${batch.length} чанков)`);
const batchResult = await this.deltaApi.uploadChunkBatch(projectId, {
batchIndex: Math.floor(i / batchSize),
totalBatches: totalBatches,
chunks: batch
});
processedTotal += batchResult.processed;
if (batchResult.errors && batchResult.errors.length > 0) {
errors.push(...batchResult.errors);
}
// ✅ Задержка перед следующим батчем для предотвращения Rate Limit
const delayMs = options.batchDelayMs ?? 0;
if (delayMs > 0 && i + batchSize < encryptedChunks.length) {
this.logger.log(`⏱️ Задержка ${delayMs}ms перед батчем ${batchNumber + 1}...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`❌ Ошибка отправки батча ${batchNumber}: ${errorMessage}`);
errors.push(`Batch ${batchNumber}: ${errorMessage}`);
}
}
// 3. Очистка удаленных файлов (если включена)
if (options.enableCleanup !== false && options.activeFiles) {
this.logger.log(`🧹 Начинаю очистку удаленных файлов`);
// ✅ Логируем deletedFiles для диагностики
if (options.deletedFiles && options.deletedFiles.length > 0) {
this.logger.log(`🗑️ Удаленные файлы от клиента: ${options.deletedFiles.join(', ')}`);
}
try {
// ✅ Передаем deletedFiles в cleanup API
const cleanupResult = await this.deltaApi.cleanupFiles(projectId, options.activeFiles, options.deletedFiles // ← НОВОЕ: передаем deletedFiles
);
if (cleanupResult.success) {
this.logger.log(`✅ Очистка завершена: удалено ${cleanupResult.deletedFromQdrant} векторов из Qdrant, ` +
`${cleanupResult.deletedFromPostgres} чанков из PostgreSQL за ${cleanupResult.cleanupTime}ms`);
}
else {
this.logger.warn(`⚠️ Очистка выполнена частично`);
}
}
catch (cleanupError) {
// Cleanup не должен ломать основной процесс
const cleanupErrorMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
this.logger.warn(`⚠️ Ошибка очистки (нескритично): ${cleanupErrorMessage}`);
}
}
else if (options.enableCleanup !== false) {
this.logger.warn(`⚠️ Cleanup пропущен - не переданы activeFiles. Передайте activeFiles в options для автоматической очистки`);
}
// 4. Финализация
await this.deltaApi.finalizeSync(projectId);
const duration = Date.now() - startTime;
this.logger.log(`✅ Синхронизация завершена за ${duration}ms: ${processedTotal}/${encryptedChunks.length} чанков`);
return {
success: errors.length === 0,
projectId,
processedChunks: processedTotal,
totalChunks: encryptedChunks.length,
duration,
sessionId: initResult.sessionId,
error: errors.length > 0 ? errors.join('; ') : undefined,
details: {
newFiles: 0,
changedFiles: 0,
deletedFiles: 0
}
};
}
catch (error) {
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`❌ Ошибка синхронизации: ${errorMessage}`);
return {
success: false,
projectId,
processedChunks: 0,
totalChunks: encryptedChunks.length,
duration,
error: errorMessage,
details: {
newFiles: 0,
changedFiles: 0,
deletedFiles: 0
}
};
}
}
/**
* 📊 Получение статуса синхронизации
*/
async getSyncStatus(projectId) {
this.ensureEnabled();
return await this.deltaApi.getSyncStatus(projectId);
}
/**
* ❌ Отмена активной синхронизации
*/
async cancelSync(projectId) {
this.ensureEnabled();
const result = await this.deltaApi.cancelSync(projectId);
return result.success;
}
/**
* 🧹 Очистка удаленных файлов из векторной базы
*/
async cleanupDeletedFiles(projectId, currentFiles) {
try {
this.logger.log(`🧹 Начинаю очистку удаленных файлов для проекта ${projectId}`);
// Подготавливаем список активных файлов
const activeFiles = currentFiles.map(file => ({
filePath: file.path,
fileHash: this.utils.calculateFileHash(file.content),
lastModified: file.lastModified || Date.now()
}));
this.logger.log(`📋 Отправляю список из ${activeFiles.length} активных файлов для очистки`);
// Вызываем API очистки
const result = await this.deltaApi.cleanupFiles(projectId, activeFiles);
if (result.success) {
this.logger.log(`✅ Очистка завершена: удалено ${result.deletedFromQdrant} векторов из Qdrant, ` +
`${result.deletedFromPostgres} чанков из PostgreSQL за ${result.cleanupTime}ms`);
}
else {
this.logger.warn(`⚠️ Очистка выполнена частично`);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`❌ Ошибка очистки удаленных файлов: ${errorMessage}`);
return {
success: false,
deletedFromQdrant: 0,
deletedFromPostgres: 0,
activeFiles: currentFiles.length,
cleanupTime: 0
};
}
}
/**
* 🧹 Публичный метод для прямой очистки удаленных файлов
* @param projectId ID проекта
* @param activeFiles Список активных файлов
* @param deletedFiles ОПЦИОНАЛЬНО: Явно удаленные файлы (пути)
*/
async performCleanup(projectId, activeFiles, deletedFiles) {
this.ensureEnabled();
return await this.deltaApi.cleanupFiles(projectId, activeFiles, deletedFiles);
}
/**
* 🗑️ Инвалидировать chunks измененного файла
*
* Удаляет устаревшие chunks из Qdrant и инвалидирует RAG cache.
* После инвалидации отправьте новые chunks через syncEncryptedChunks().
*
* @param projectId - UUID проекта
* @param options - Опции инвалидации (filePath обязателен)
* @returns Результат операции с количеством инвалидированных chunks
*
* @example
* ```typescript
* // Инвалидировать файл после изменения
* const result = await sdk.deltaManager.invalidateFile(projectId, {
* filePath: 'src/app.tsx',
* reason: 'file_changed'
* });
*
* if (result.success) {
* console.log(`Invalidated ${result.chunksInvalidated} chunks`);
* }
*
* // Теперь отправить новые chunks
* await sdk.deltaManager.syncEncryptedChunks(projectId, newChunks, rootHash);
* ```
*/
async invalidateFile(projectId, options) {
this.ensureEnabled();
if (!options.filePath || !options.filePath.trim()) {
throw new Error('filePath is required for file invalidation');
}
this.logger.log(`🗑️ Инвалидирую файл: ${options.filePath} (причина: ${options.reason || 'file_changed'})`);
try {
const result = await this.deltaApi.invalidateFile(projectId, options);
if (result.success) {
this.logger.log(`✅ Файл инвалидирован: ${result.chunksInvalidated} chunks удалено, ` +
`cache инвалидирован: ${result.cacheInvalidated}`);
}
else {
this.logger.warn(`⚠️ Инвалидация файла выполнена с предупреждениями`);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`❌ Ошибка инвалидации файла: ${errorMessage}`);
// Graceful degradation: не бросаем ошибку, backend System Prompt recovery подхватит
return {
success: false,
chunksInvalidated: 0,
cacheInvalidated: false,
filePath: options.filePath
};
}
}
/**
* 🗑️ Batch инвалидация файлов (для предотвращения rate limiting)
*
* Эффективная инвалидация множества файлов за один API запрос.
* Используйте этот метод вместо множественных вызовов invalidateFile()
* для предотвращения превышения rate limit сервера (30 req/sec).
*
* @param projectId - UUID проекта
* @param files - Массив файлов для инвалидации
* @returns Результат batch операции с общей статистикой
*
* @example
* ```typescript
* // Инвалидировать несколько файлов одним запросом
* const result = await sdk.deltaManager.invalidateFiles(projectId, [
* { filePath: 'src/app.tsx', reason: 'file_changed' },
* { filePath: 'src/utils.ts', reason: 'file_saved' },
* { filePath: 'src/config.json', reason: 'file_changed' }
* ]);
*
* console.log(`Invalidated ${result.totalChunksInvalidated} chunks from ${result.totalFiles} files`);
*
* // Теперь отправить новые chunks
* await sdk.deltaManager.syncEncryptedChunks(projectId, newChunks, rootHash);
* ```
*/
async invalidateFiles(projectId, files) {
this.ensureEnabled();
if (!files || files.length === 0) {
throw new Error('files array is required and must not be empty');
}
this.logger.log(`🗑️ Batch инвалидация: ${files.length} файлов`);
try {
const result = await this.deltaApi.invalidateFiles(projectId, files);
if (result.success) {
this.logger.log(`✅ Batch инвалидация завершена: ${result.totalFiles} файлов, ` +
`${result.totalChunksInvalidated} chunks удалено, ` +
`${result.totalCacheInvalidated} cache записей инвалидировано`);
}
else {
this.logger.warn(`⚠️ Batch инвалидация выполнена с предупреждениями`);
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`❌ Ошибка batch инвалидации: ${errorMessage}`);
// Graceful degradation
return {
success: false,
totalFiles: files.length,
totalChunksInvalidated: 0,
totalCacheInvalidated: 0,
results: files.map(f => ({
success: false,
chunksInvalidated: 0,
cacheInvalidated: false,
filePath: f.filePath,
error: errorMessage
}))
};
}
}
/**
* Проверка что delta-chunking включен
*/
ensureEnabled() {
if (!this.isEnabled) {
throw new Error('Delta-chunking не включен. Установите deltaChunking.enabled = true в конфигурации SDK');
}
}
}
exports.DeltaChunkingManager = DeltaChunkingManager;
//# sourceMappingURL=delta-chunking-manager.js.map