import { fromAsyncThrowable, ResultAsync, errAsync } from 'neverthrow'
type ApiError = 'NetworkError' | 'Timeout' | 'RateLimited'
const callApi = fromAsyncThrowable(
async (endpoint: string) => {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
try {
const response = await fetch(endpoint, { signal: controller.signal })
return await response.json()
} finally {
clearTimeout(timeout)
}
},
(error): ApiError => {
if (error instanceof Error) {
if (error.name === 'AbortError') return 'Timeout'
if (error.message.includes('rate limit')) return 'RateLimited'
}
return 'NetworkError'
}
)
async function callWithRetry(
endpoint: string,
maxRetries: number = 3
): ResultAsync<any, ApiError> {
let lastError: ApiError = 'NetworkError'
for (let i = 0; i < maxRetries; i++) {
const result = await callApi(endpoint)
if (result.isOk()) {
return result
}
lastError = result.error
// Don't retry on rate limit
if (result.error === 'RateLimited') {
break
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
return errAsync(lastError)
}