Skip to main content
Glama
provider-operation-handler.ts14.9 kB
/** * Provider Operation Handler * * Unified interface for handling operations across multiple providers. * Supports provider="both" with parallel execution, result aggregation, * and partial failure handling. */ import { BaseProvider, MultiProvider } from './base-provider.js'; import { ProviderFactory, ProviderType } from './provider-factory.js'; import { ProviderConfig, ProviderResult, ProviderOperation } from './types.js'; import { configManager } from '../config.js'; import { RepositorySynchronizer } from '../utils/repository-sync.js'; import { DataMerger } from '../utils/data-merger.js'; export interface OperationResult { success: boolean; results: ProviderResult[]; partialFailure: boolean; errors: ProviderResult[]; data?: any; // UNIVERSAL MODE: Dados mesclados para operações de leitura metadata: { operation: string; provider: string; executionTime: number; timestamp: string; }; } export class ProviderOperationHandler { private factory: ProviderFactory; private config: ProviderConfig; private repositorySync: RepositorySynchronizer; constructor(config: ProviderConfig) { this.config = config; this.factory = new ProviderFactory(config); this.repositorySync = new RepositorySynchronizer(config); } /** * Execute operation with unified interface */ async executeOperation(operation: ProviderOperation): Promise<OperationResult> { const startTime = Date.now(); try { // UNIVERSAL MODE: Auto-aplicar provider="both" se modo universal ativo if (configManager.isUniversalMode() && operation.provider !== 'both') { operation.provider = 'both'; console.error('[Universal Mode] Auto-applying both providers'); } // UNIVERSAL MODE: Sincronização pré-operação para operações de escrita if (configManager.isUniversalMode() && this.isWriteOperation(operation.operation)) { await this.ensureRepositorySynchronized(operation); } const provider = this.factory.createProviderFromString(operation.provider); let result: OperationResult; if (this.isMultiProvider(provider)) { result = await this.executeMultiProviderOperation(provider, operation, startTime); // UNIVERSAL MODE: Mesclagem pós-operação para operações de leitura if (configManager.isUniversalMode() && this.isReadOperation(operation.operation)) { result.data = this.mergeOperationResults(operation.operation, result.results); } // UNIVERSAL MODE: Tentativa de recuperação em falha parcial if (result.partialFailure && configManager.isUniversalMode()) { await this.attemptAutoRecovery(operation, result); } } else { result = await this.executeSingleProviderOperation(provider, operation, startTime); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return this.formatOperationError(operation, startTime, errorMessage, error); } } /** * Execute operation on single provider */ private async executeSingleProviderOperation( provider: BaseProvider, operation: ProviderOperation, startTime: number ): Promise<OperationResult> { try { // Auto-fill repo from projectPath when missing if (operation.parameters && !operation.parameters.repo && operation.parameters.projectPath) { try { const { repositoryDetector } = await import('../utils/repository-detector.js'); const context = await repositoryDetector.getProjectContext(operation.parameters.projectPath); if (context && context.autoDetectedSettings) { if (!operation.parameters.repo && context.autoDetectedSettings.repository) { operation.parameters.repo = context.autoDetectedSettings.repository; } } } catch { // ignore auto-detection errors and proceed, provider will return meaningful error } } const result = await provider.executeOperation(operation.operation, operation.parameters); return { success: result.success, results: [result], partialFailure: false, errors: result.success ? [] : [result], metadata: { operation: operation.operation, provider: operation.provider, executionTime: Date.now() - startTime, timestamp: new Date().toISOString() } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorResult: ProviderResult = { success: false, error: { code: 'PROVIDER_EXECUTION_ERROR', message: errorMessage, details: error }, provider: operation.provider }; return { success: false, results: [], partialFailure: false, errors: [errorResult], metadata: { operation: operation.operation, provider: operation.provider, executionTime: Date.now() - startTime, timestamp: new Date().toISOString() } }; } } /** * Execute operation on multiple providers in parallel */ private async executeMultiProviderOperation( multiProvider: MultiProvider, operation: ProviderOperation, startTime: number ): Promise<OperationResult> { try { const results = await multiProvider.executeOperation(operation.operation, operation.parameters); const successResults = results.filter(r => r.success); const errorResults = results.filter(r => !r.success); const hasSuccess = successResults.length > 0; const hasErrors = errorResults.length > 0; const partialFailure = hasSuccess && hasErrors; return { success: hasSuccess, results: successResults, partialFailure, errors: errorResults, metadata: { operation: operation.operation, provider: operation.provider, executionTime: Date.now() - startTime, timestamp: new Date().toISOString() } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return this.formatOperationError(operation, startTime, errorMessage, error); } } /** * Validate operation before execution */ validateOperation(operation: ProviderOperation): { valid: boolean; errors: string[]; warnings: string[]; } { const errors: string[] = []; const warnings: string[] = []; // Validate provider if (!operation.provider) { errors.push('Provider is required'); } else if (!['github', 'gitea', 'both'].includes(operation.provider)) { errors.push(`Invalid provider: ${operation.provider}. Valid options: github, gitea, both`); } // Validate operation name if (!operation.operation) { errors.push('Operation is required'); } // Validate parameters if (!operation.parameters) { errors.push('Parameters are required'); } // Check provider availability if (operation.provider === 'github' && !this.factory.isProviderAvailable('github')) { errors.push('GitHub provider is not configured'); } if (operation.provider === 'gitea' && !this.factory.isProviderAvailable('gitea')) { errors.push('Gitea provider is not configured'); } if (operation.provider === 'both') { const availableProviders = this.factory.getAvailableProviders(); if (availableProviders.length === 0) { errors.push('No providers are configured'); } else if (availableProviders.length === 1) { warnings.push(`Only ${availableProviders[0]} provider is configured, 'both' will only use one provider`); } } // Validate authentication requirements if (operation.requiresAuth) { if (operation.provider === 'github' && !this.config.github?.token) { errors.push('GitHub authentication is required for this operation'); } if (operation.provider === 'gitea' && !this.config.gitea?.token) { errors.push('Gitea authentication is required for this operation'); } } return { valid: errors.length === 0, errors, warnings }; } /** * Get provider status and configuration */ getProviderStatus(): { github: { configured: boolean; available: boolean; missingFields: string[]; }; gitea: { configured: boolean; available: boolean; missingFields: string[]; }; multiProvider: { available: boolean; configuredProviders: string[]; }; } { const githubValidation = this.factory.validateProviderConfig('github'); const giteaValidation = this.factory.validateProviderConfig('gitea'); const availableProviders = this.factory.getAvailableProviders(); return { github: { configured: githubValidation.valid, available: this.factory.isProviderAvailable('github'), missingFields: githubValidation.missingFields }, gitea: { configured: giteaValidation.valid, available: this.factory.isProviderAvailable('gitea'), missingFields: giteaValidation.missingFields }, multiProvider: { available: availableProviders.length > 1, configuredProviders: availableProviders } }; } /** * Aggregate results from multiple providers */ aggregateResults(results: ProviderResult[]): { combinedData: any; summary: { totalProviders: number; successfulProviders: number; failedProviders: number; providers: string[]; }; } { const successResults = results.filter(r => r.success); const failedResults = results.filter(r => !r.success); // Combine data from successful results let combinedData: any = {}; if (successResults.length === 1) { combinedData = successResults[0].data; } else if (successResults.length > 1) { // For multiple results, create an object with provider-specific data combinedData = {}; successResults.forEach(result => { combinedData[result.provider] = result.data; }); } return { combinedData, summary: { totalProviders: results.length, successfulProviders: successResults.length, failedProviders: failedResults.length, providers: results.map(r => r.provider) } }; } /** * Handle partial failures with retry logic */ async handlePartialFailure( operation: ProviderOperation, results: ProviderResult[], retryOptions?: { maxRetries: number; retryDelay: number; retryFailedOnly: boolean; } ): Promise<OperationResult> { const options = { maxRetries: 1, retryDelay: 1000, retryFailedOnly: true, ...retryOptions }; const failedResults = results.filter(r => !r.success); if (failedResults.length === 0 || options.maxRetries === 0) { // No failures or no retries requested return this.executeOperation(operation); } // Wait before retry await new Promise(resolve => setTimeout(resolve, options.retryDelay)); if (options.retryFailedOnly) { // Retry only failed providers const failedProviders = failedResults.map(r => r.provider); const retryOperation: ProviderOperation = { ...operation, provider: failedProviders.length === 1 ? failedProviders[0] as any : 'both' }; return this.executeOperation(retryOperation); } else { // Retry entire operation return this.executeOperation(operation); } } /** * Check if provider is multi-provider */ private isMultiProvider(provider: BaseProvider | MultiProvider): provider is MultiProvider { return 'providers' in provider; } /** * Check if operation is a write operation */ private isWriteOperation(operation: string): boolean { const writeOps = ['create', 'update', 'delete', 'merge', 'close', 'publish']; return writeOps.some(op => operation.includes(op)); } /** * Check if operation is a read operation */ private isReadOperation(operation: string): boolean { const readOps = ['list', 'get', 'search', 'show']; return readOps.some(op => operation.includes(op)); } /** * Ensure repository is synchronized before write operations */ private async ensureRepositorySynchronized(operation: ProviderOperation): Promise<void> { if (operation.parameters.repo) { if (operation.provider === 'both') { // For 'both' provider, ensure repository exists on both GitHub and Gitea await Promise.all([ this.repositorySync.ensureRepositoryExists(operation.parameters.repo, 'github'), this.repositorySync.ensureRepositoryExists(operation.parameters.repo, 'gitea') ]); } else { await this.repositorySync.ensureRepositoryExists( operation.parameters.repo, operation.provider ); } } } /** * Merge operation results for read operations */ private mergeOperationResults(operation: string, results: ProviderResult[]): any { if (operation.includes('list')) { return DataMerger.mergeListResults(results); } else if (operation.includes('get')) { return DataMerger.mergeGetResults(results); } else if (operation.includes('search')) { return DataMerger.mergeSearchResults(results); } return results; } /** * Attempt automatic recovery for partial failures */ private async attemptAutoRecovery( operation: ProviderOperation, result: OperationResult ): Promise<void> { for (const error of result.errors) { const recovered = await this.repositorySync.attemptRecovery( operation.operation, operation.parameters, error.provider ); if (recovered) { console.error(`[Universal Mode] Auto-recovered ${error.provider}`); } } } /** * Format operation error */ private formatOperationError( operation: ProviderOperation, startTime: number, message: string, details?: any ): OperationResult { const errorResult: ProviderResult = { success: false, error: { code: 'OPERATION_ERROR', message, details }, provider: operation.provider }; return { success: false, results: [], partialFailure: false, errors: [errorResult], metadata: { operation: operation.operation, provider: operation.provider, executionTime: Date.now() - startTime, timestamp: new Date().toISOString() } }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Andre-Buzeli/git-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server