UNPKG

crewai-ts

Version:

TypeScript port of crewAI for agent-based workflows

429 lines (428 loc) 13.9 kB
/** * Async utilities implementation * Optimized for efficient asynchronous operations with concurrency control */ import { TimeoutError } from './errors.js'; /** * Sequential map - process items one after another */ export async function mapSequential(items, fn) { const results = []; for (let i = 0; i < items.length; i++) { // Ensure item is not undefined before passing to fn const item = items[i]; if (item !== undefined) { results.push(await fn(item, i)); } else { // Handle undefined items by skipping or providing a default value // This approach maintains type safety continue; } } return results; } /** * Process items in parallel with concurrency control */ export async function mapLimit(items, concurrency, fn) { if (concurrency <= 0) { throw new Error('Concurrency must be greater than 0'); } if (items.length === 0) { return []; } // Use sequential processing if concurrency is 1 if (concurrency === 1) { return mapSequential(items, fn); } const results = new Array(items.length); const inProgress = new Set(); let nextIndex = 0; // Process next item const processNext = async () => { if (nextIndex >= items.length) { return; } const index = nextIndex++; const item = items[index]; try { // Ensure item is properly typed results[index] = await fn(item, index); } catch (error) { // Re-throw to be caught by the promise created below throw { error, index }; } }; // Create initial batch of promises const initialBatchSize = Math.min(concurrency, items.length); for (let i = 0; i < initialBatchSize; i++) { const promise = processNext().catch(e => e); inProgress.add(promise); // Clean up when promise completes promise.finally(() => { inProgress.delete(promise); }); } // Process remaining items as others complete while (inProgress.size > 0) { // Map each promise to return itself when resolved with proper typing const promises = Array.from(inProgress).map(p => p.then(result => ({ promise: p, result, isError: false })) .catch(error => ({ promise: p, error, isError: true }))); // Now race will return an object with the promise and its result/error const completed = await Promise.race(promises); // Handle the completed promise in a type-safe way // Use type guard pattern that TypeScript understands if (completed.isError === true) { // With the discriminated union pattern, TypeScript knows this is ErrorResult throw completed.error; } // Remove the completed promise from the set inProgress.delete(completed.promise); // Start a new task if there are more items if (nextIndex < items.length) { const promise = processNext().catch(e => e); inProgress.add(promise); // Clean up when promise completes promise.finally(() => { inProgress.delete(promise); }); } } return results; } /** * Process items in batches with concurrency control */ export async function batchProcess(items, processFn, options = {}) { const concurrency = options.concurrency ?? 5; const batchSize = options.batchSize ?? concurrency; const batchDelayMs = options.batchDelayMs ?? 0; if (items.length === 0) { return []; } // Calculate total number of batches const numBatches = Math.ceil(items.length / batchSize); const results = new Array(items.length); // Process each batch for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) { const start = batchIndex * batchSize; const end = Math.min(start + batchSize, items.length); const batch = items.slice(start, end); // Notify batch start if callback provided if (options.onBatchStart) { await Promise.resolve(options.onBatchStart(batch, batchIndex)); } // Process batch with concurrency const processBatchWithConcurrency = async () => { try { const batchResults = await mapLimit(batch, Math.min(concurrency, batch.length), async (item, batchItemIndex) => { const originalIndex = start + batchItemIndex; try { return await processFn(item, originalIndex); } catch (error) { // Handle error if callback provided if (options.onError) { const shouldContinue = await Promise.resolve(options.onError(error, item, originalIndex)); // Explicitly check against false, as undefined or null should continue if (shouldContinue === false) { throw error; // Re-throw to abort processing } // Return null or undefined for errors that are handled return null; } throw error; // Re-throw if no error handler } }); // Store batch results for (let i = 0; i < batchResults.length; i++) { results[start + i] = batchResults[i]; } // Notify batch completion if callback provided if (options.onBatchComplete) { await Promise.resolve(options.onBatchComplete(batchResults, batchIndex)); } return batchResults; } catch (error) { // Propagate error throw error; } }; // Process current batch await processBatchWithConcurrency(); // Delay between batches if specified if (batchDelayMs > 0 && batchIndex < numBatches - 1) { await new Promise(resolve => setTimeout(resolve, batchDelayMs)); } } return results; } /** * Apply a timeout to a promise * Returns a new promise that rejects with TimeoutError if the original promise doesn't settle within the specified timeout */ export function withTimeout(promise, timeoutMs, operationName = 'operation') { if (timeoutMs <= 0) { return promise; } return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new TimeoutError(`Operation timed out after ${timeoutMs}ms`, { operationName, timeoutMs })); }, timeoutMs); promise .then(result => { clearTimeout(timeoutId); resolve(result); }) .catch(error => { clearTimeout(timeoutId); reject(error); }); }); } /** * Pool for managing async tasks with concurrency limits and priorities */ export class TaskPool { concurrency; queue = []; activeCount = 0; isPaused = false; constructor(concurrency = 5) { this.concurrency = concurrency; if (concurrency <= 0) { throw new Error('Concurrency must be greater than 0'); } } /** * Schedule a task for execution */ async schedule(task, priority = 0) { return new Promise((resolve, reject) => { this.queue.push({ task, priority, resolve, reject }); this.processNext(); }); } /** * Get the current size of the queue */ get size() { return this.queue.length; } /** * Get the number of active tasks */ get active() { return this.activeCount; } /** * Pause task processing */ pause() { this.isPaused = true; } /** * Resume task processing */ resume() { if (this.isPaused) { this.isPaused = false; this.processNext(); } } /** * Change the concurrency limit */ setConcurrency(concurrency) { if (concurrency <= 0) { throw new Error('Concurrency must be greater than 0'); } this.concurrency = concurrency; this.processNext(); } /** * Clear all pending tasks */ clear() { const error = new Error('Task canceled due to pool clearing'); for (const item of this.queue) { item.reject(error); } this.queue = []; } /** * Wait for all tasks to complete */ async drain() { if (this.queue.length === 0 && this.activeCount === 0) { return; } return new Promise(resolve => { const checkComplete = () => { if (this.queue.length === 0 && this.activeCount === 0) { resolve(); } else { setTimeout(checkComplete, 100); } }; checkComplete(); }); } /** * Process next tasks in the queue */ processNext() { if (this.isPaused || this.queue.length === 0 || this.activeCount >= this.concurrency) { return; } // Sort queue by priority (higher priority first) this.queue.sort((a, b) => b.priority - a.priority); // Calculate how many tasks we can start const availableSlots = Math.min(this.concurrency - this.activeCount, this.queue.length); // Start tasks for (let i = 0; i < availableSlots; i++) { const item = this.queue.shift(); this.activeCount++; Promise.resolve().then(async () => { try { const result = await item.task(); item.resolve(result); } catch (error) { item.reject(error); } finally { this.activeCount--; this.processNext(); } }); } } } /** * Create a debounced version of a function */ export function debounce(fn, wait, options = {}) { let timeout = null; let lastArgs = null; let lastCallTime = 0; let lastInvokeTime = 0; let leadingEdge = false; const leading = options.leading ?? false; const trailing = options.trailing ?? true; const maxWait = options.maxWait; function invokeFunc() { const args = lastArgs; lastArgs = null; lastInvokeTime = Date.now(); if (args) { return fn(...args); } return undefined; } function startTimer(pendingFunc, wait) { if (timeout !== null) { clearTimeout(timeout); } timeout = setTimeout(pendingFunc, wait); } function shouldInvoke() { const now = Date.now(); const timeSinceLastCall = now - lastCallTime; const timeSinceLastInvoke = now - lastInvokeTime; return (lastCallTime === 0 || // First call timeSinceLastCall >= wait || // Regular timeout elapsed (maxWait !== undefined && timeSinceLastInvoke >= maxWait) // Force invoke if maxWait is reached ); } function trailingEdge() { timeout = null; if (trailing && lastArgs) { return invokeFunc(); } lastArgs = null; return undefined; } function timerExpired() { const now = Date.now(); const isInvoking = shouldInvoke(); if (isInvoking) { return trailingEdge(); } // Restart the timer if (maxWait !== undefined) { const timeSinceLastInvoke = now - lastInvokeTime; const timeUntilMaxWait = maxWait - timeSinceLastInvoke; const timeUntilWait = wait - (now - lastCallTime); const remainingWait = Math.min(timeUntilMaxWait, timeUntilWait); startTimer(timerExpired, remainingWait); } else { startTimer(timerExpired, wait - (now - lastCallTime)); } return undefined; } function debounced(...args) { const now = Date.now(); lastArgs = args; lastCallTime = now; if (timeout === null) { leadingEdge = true; } const isInvoking = shouldInvoke(); if (isInvoking) { if (timeout === null) { if (leadingEdge && leading) { lastInvokeTime = lastCallTime; startTimer(timerExpired, wait); leadingEdge = false; return invokeFunc(); } if (maxWait !== undefined) { startTimer(timerExpired, maxWait); } } if (trailing) { startTimer(timerExpired, wait); } return undefined; } if (timeout === null) { startTimer(timerExpired, wait); } return undefined; } debounced.cancel = function () { if (timeout !== null) { clearTimeout(timeout); } lastInvokeTime = 0; lastCallTime = 0; lastArgs = null; timeout = null; leadingEdge = false; }; return debounced; } /** * Create a throttled version of a function */ export function throttle(fn, wait, options = {}) { return debounce(fn, wait, { leading: options.leading ?? true, trailing: options.trailing ?? true, maxWait: wait }); }