harb/web-app/src/utils/graphqlRetry.ts
2025-10-11 10:55:49 +00:00

107 lines
3.2 KiB
TypeScript

import { ref, type Ref } from 'vue';
import axios from 'axios';
import { chainConfigService } from '@/services/chainConfig';
const RETRY_BASE_DELAY_MS = 1_500;
const RETRY_MAX_DELAY_MS = 60_000;
export interface RetryManager {
schedule: () => void;
clear: () => void;
reset: () => void;
retryDelayMs: Ref<number>;
}
/**
* Creates a retry manager with exponential backoff for async operations.
*
* @param loadFn - The async function to retry (receives activeChainId as argument)
* @param activeChainId - Ref containing the current chain ID to pass to loadFn
* @param baseDelay - Initial retry delay in milliseconds (default: 1500ms)
* @param maxDelay - Maximum retry delay in milliseconds (default: 60000ms)
* @returns RetryManager with schedule, clear, and reset methods
*
* @example
* const retry = createRetryManager(loadPositions, activeChainId);
* try {
* await loadPositions(chainId);
* retry.reset(); // Reset on success
* } catch (error) {
* retry.schedule(); // Schedule retry with backoff
* }
*/
export function createRetryManager(
loadFn: (chainId: number) => Promise<void>,
activeChainId: Ref<number>,
baseDelay = RETRY_BASE_DELAY_MS,
maxDelay = RETRY_MAX_DELAY_MS
): RetryManager {
const retryDelayMs = ref(baseDelay);
let retryTimer: number | null = null;
function clear() {
if (retryTimer !== null) {
clearTimeout(retryTimer);
retryTimer = null;
}
}
function schedule() {
if (typeof window === 'undefined' || retryTimer !== null) {
return;
}
const delay = retryDelayMs.value;
retryTimer = window.setTimeout(async () => {
retryTimer = null;
await loadFn(activeChainId.value);
}, delay);
retryDelayMs.value = Math.min(retryDelayMs.value * 2, maxDelay);
}
function reset() {
retryDelayMs.value = baseDelay;
}
return { schedule, clear, reset, retryDelayMs };
}
/**
* Formats GraphQL/Axios errors into user-friendly messages.
*
* @param error - The error object from axios or other source
* @returns Formatted error message string
*/
export function formatGraphqlError(error: unknown): string {
if (axios.isAxiosError(error)) {
const responseErrors = (error.response?.data as { errors?: unknown[] })?.errors;
if (Array.isArray(responseErrors) && responseErrors.length > 0) {
return responseErrors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', ');
}
if (error.response?.status) {
return `GraphQL request failed with status ${error.response.status}`;
}
if (error.message) {
return error.message;
}
}
if (error instanceof Error && error.message) {
return error.message;
}
return 'Unknown GraphQL error';
}
/**
* Resolves a GraphQL endpoint URL for a given chain ID.
*
* @param chainId - The blockchain network ID
* @param endpointOverride - Optional endpoint URL to use instead of default
* @returns The GraphQL endpoint URL
* @throws Error if endpoint is not configured for the chain
*/
export function resolveGraphqlEndpoint(chainId: number, endpointOverride?: string): string {
const normalized = endpointOverride?.trim();
if (normalized) {
return normalized;
}
return chainConfigService.getEndpoint(chainId, 'graphql');
}