ClickUp MCP Server
by windalfin
Verified
/**
* ClickUp Bulk Operations Service – infrastructure for bulk operations with batching, error handling, progress tracking, and configurable concurrency.
*/
import { ClickUpServiceError, ErrorCode } from './base.js';
import { AxiosError } from 'axios';
export interface BulkOperationOptions {
batchSize?: number; // default 10
concurrency?: number; // default 3
continueOnError?: boolean; // default false
retryCount?: number; // default 3
retryDelay?: number; // default 1000
exponentialBackoff?: boolean; // default true
onProgress?: (completed: number, total: number, success: number, failed: number) => void;
}
export interface ProgressInfo {
totalItems: number;
completedItems: number;
failedItems: number;
currentBatch: number;
totalBatches: number;
percentComplete: number;
context?: Record<string, any>;
}
export interface BulkOperationResult<T> {
success: boolean;
successfulItems: T[];
failedItems: Array<{ item: any; index: number; error: Error }>;
totalItems: number;
successCount: number;
failureCount: number;
}
export class BulkProcessor {
public async processBulk<T, R>(
items: T[],
processor: (item: T, index: number) => Promise<R>,
options?: BulkOperationOptions
): Promise<BulkOperationResult<R>> {
const opts: Required<BulkOperationOptions> = {
batchSize: options?.batchSize ?? 10,
concurrency: options?.concurrency ?? 3,
continueOnError: options?.continueOnError ?? false,
retryCount: options?.retryCount ?? 3,
retryDelay: options?.retryDelay ?? 1000,
exponentialBackoff: options?.exponentialBackoff ?? true,
onProgress: options?.onProgress ?? (() => {})
};
const result: BulkOperationResult<R> = {
success: true,
successfulItems: [],
failedItems: [],
totalItems: items.length,
successCount: 0,
failureCount: 0
};
if (items.length === 0) return result;
try {
const totalBatches = Math.ceil(items.length / opts.batchSize);
let processedItems = 0;
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
const startIdx = batchIndex * opts.batchSize;
const endIdx = Math.min(startIdx + opts.batchSize, items.length);
const batch = items.slice(startIdx, endIdx);
const batchResults = await this.processBatch(batch, processor, startIdx, opts);
result.successfulItems.push(...batchResults.successfulItems);
result.failedItems.push(...batchResults.failedItems);
result.successCount += batchResults.successCount;
result.failureCount += batchResults.failureCount;
if (batchResults.failureCount > 0 && !opts.continueOnError) {
result.success = false;
return result;
}
processedItems += batch.length;
opts.onProgress(processedItems, items.length, result.successCount, result.failureCount);
}
result.success = result.failedItems.length === 0;
return result;
} catch (error) {
const err = error as Error;
console.error('Failed to process bulk operation:', err.message || String(error));
result.success = false;
return result;
}
}
private async processBatch<T, R>(
batch: T[],
processor: (item: T, index: number) => Promise<R>,
startIndex: number,
opts: Required<BulkOperationOptions>
): Promise<BulkOperationResult<R>> {
const result: BulkOperationResult<R> = {
success: true,
successfulItems: [],
failedItems: [],
totalItems: batch.length,
successCount: 0,
failureCount: 0
};
try {
for (let i = 0; i < batch.length; i += opts.concurrency) {
const concurrentBatch = batch.slice(i, Math.min(i + opts.concurrency, batch.length));
const promises = concurrentBatch.map((item, idx) => {
const index = startIndex + i + idx;
return this.processWithRetry(() => processor(item, index), index, item, opts);
});
const results = await Promise.allSettled(promises);
results.forEach((promiseResult, idx) => {
const index = startIndex + i + idx;
if (promiseResult.status === 'fulfilled') {
result.successfulItems.push(promiseResult.value);
result.successCount++;
} else {
const error = promiseResult.reason as Error;
result.failedItems.push({ item: batch[i + idx], index, error });
result.failureCount++;
if (!opts.continueOnError) {
result.success = false;
throw new Error(`Bulk operation failed at index ${index}: ${error.message || String(error)}`);
}
}
});
}
return result;
} catch (error) {
const err = error as Error;
console.error(`Bulk operation failed: ${err.message || String(error)}`, error);
result.success = false;
return result;
}
}
private async processWithRetry<R>(
operation: () => Promise<R>,
index: number,
item: any,
options: Required<BulkOperationOptions>
): Promise<R> {
let attempts = 1;
let lastError: Error = new Error('Unknown error');
while (attempts <= options.retryCount) {
try {
return await operation();
} catch (error) {
const err = error as Error;
console.warn(`Operation failed for item at index ${index}, attempt ${attempts}/${options.retryCount}: ${err.message || String(error)}`);
lastError = err;
if (attempts >= options.retryCount) break;
const delay = options.exponentialBackoff
? options.retryDelay * Math.pow(2, attempts) + Math.random() * 1000
: options.retryDelay * Math.pow(1.5, attempts - 1);
await new Promise(resolve => setTimeout(resolve, delay));
attempts++;
}
}
throw new Error(`Operation failed after ${attempts} attempts for item at index ${index}: ${lastError?.message || 'Unknown error'}`);
}
}