UNPKG

obsidian-mcp-server

Version:

Obsidian Knowledge-Management MCP (Model Context Protocol) server that enables AI agents and development tools to interact with an Obsidian vault. It provides a comprehensive suite of tools for reading, writing, searching, and managing notes, tags, and fr

102 lines (101 loc) 5.5 kB
/** * @fileoverview Provides utilities for handling asynchronous operations, * such as retrying operations with delays. * @module src/utils/internal/asyncUtils */ import { McpError, BaseErrorCode } from "../../types-global/errors.js"; import { logger } from "./logger.js"; /** * Executes an asynchronous operation with a configurable retry mechanism. * This function will attempt the operation up to `maxRetries` times, with a specified * `delayMs` between attempts. It allows for custom logic to decide if an error * warrants a retry and for actions to be taken before each retry. * * @template T The expected return type of the asynchronous operation. * @param {() => Promise<T>} operation - The asynchronous function to execute. * This function should return a Promise resolving to type `T`. * @param {RetryConfig<T>} config - Configuration options for the retry behavior, * including operation name, context, retry limits, delay, and custom handlers. * @returns {Promise<T>} A promise that resolves with the result of the operation if successful. * @throws {McpError} Throws an `McpError` if the operation fails after all retry attempts, * or if an unexpected error occurs during the retry logic. The error will contain details * about the operation name, context, and the last encountered error. */ export async function retryWithDelay(operation, config) { const { operationName, context, maxRetries, delayMs, shouldRetry = () => true, // Default: retry on any error onRetry, } = config; let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; // Ensure the context for logging includes attempt details const retryAttemptContext = { ...context, // Spread existing context operation: operationName, // Ensure operationName is part of the context for logger attempt, maxRetries, lastError: error instanceof Error ? error.message : String(error), }; if (attempt < maxRetries && shouldRetry(error)) { if (onRetry) { onRetry(attempt, error); // Custom onRetry logic } else { // Default logging for retry attempt logger.warning(`Operation '${operationName}' failed on attempt ${attempt} of ${maxRetries}. Retrying in ${delayMs}ms...`, retryAttemptContext); } await new Promise((resolve) => setTimeout(resolve, delayMs)); } else { // Max retries reached or shouldRetry returned false const finalErrorMsg = `Operation '${operationName}' failed definitively after ${attempt} attempt(s).`; // Log the final failure with the enriched context logger.error(finalErrorMsg, error instanceof Error ? error : undefined, retryAttemptContext); if (error instanceof McpError) { // If the last error was already an McpError, re-throw it but ensure its details are preserved/updated. error.details = { ...(typeof error.details === "object" && error.details !== null ? error.details : {}), ...retryAttemptContext, // Add retry context to existing details finalAttempt: true, }; throw error; } // For other errors, wrap in a new McpError throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, // Default to SERVICE_UNAVAILABLE, consider making this configurable or smarter `${finalErrorMsg} Last error: ${error instanceof Error ? error.message : String(error)}`, { ...retryAttemptContext, // Include all retry context originalErrorName: error instanceof Error ? error.name : typeof error, originalErrorStack: error instanceof Error ? error.stack : undefined, finalAttempt: true, }); } } } // Fallback: This part should ideally not be reached if the loop logic is correct. // If it is, it implies an issue with the loop or maxRetries logic. const fallbackErrorContext = { ...context, operation: operationName, maxRetries, reason: "Fallback_Error_Path_Reached_In_Retry_Logic", }; logger.crit( // Log as critical because this path indicates a logic flaw `Operation '${operationName}' failed unexpectedly after all retries (fallback path). This may indicate a logic error in retryWithDelay.`, lastError instanceof Error ? lastError : undefined, fallbackErrorContext); throw new McpError(BaseErrorCode.INTERNAL_ERROR, // Indicates an issue with the retry utility itself `Operation '${operationName}' failed unexpectedly after all retries (fallback path). Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`, { ...fallbackErrorContext, originalError: lastError instanceof Error ? { message: lastError.message, name: lastError.name, stack: lastError.stack, } : String(lastError), }); }