// shared/modules/google-reviews/utils.ts - Fetch Utilities and Helpers
import type { FetchRetryOptions, GeoPoint } from "./types.js";
const DEFAULT_TIMEOUT_MS = 10000; // 10 seconds
const DEFAULT_RETRIES = 2;
const DEFAULT_RETRY_DELAY_MS = 1000;
/**
* Fetch with timeout using AbortController.
* Throws error if request takes longer than timeout.
*/
export async function fetchWithTimeout(
url: string,
options: RequestInit = {},
timeout: number = DEFAULT_TIMEOUT_MS
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
return response;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Fetch with retry logic for transient failures.
* Retries on 503 errors and network failures.
*/
export async function fetchWithRetry(
url: string,
options: RequestInit = {},
retryOptions: FetchRetryOptions = {}
): Promise<Response> {
const {
retries = DEFAULT_RETRIES,
retryDelay = DEFAULT_RETRY_DELAY_MS,
timeout = DEFAULT_TIMEOUT_MS,
} = retryOptions;
let lastError: Error | undefined;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fetchWithTimeout(url, options, timeout);
// Retry on 503 Service Unavailable
if (response.status === 503 && attempt < retries) {
await sleep(retryDelay * (attempt + 1)); // Exponential backoff
continue;
}
return response;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
// Don't retry on timeout or abort
if (lastError.message.includes("timeout") || lastError.name === "AbortError") {
throw lastError;
}
// Retry on network errors
if (attempt < retries) {
await sleep(retryDelay * (attempt + 1));
continue;
}
}
}
throw lastError || new Error("Request failed after retries");
}
/**
* Sleep for specified milliseconds
*/
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Calculate Haversine distance between two geographic points (in km).
* Uses the Haversine formula for great-circle distance on a sphere.
*/
export function haversineDistance(point1: GeoPoint, point2: GeoPoint): number {
const R = 6371; // Earth's radius in km
const lat1Rad = (point1.latitude * Math.PI) / 180;
const lat2Rad = (point2.latitude * Math.PI) / 180;
const deltaLat = ((point2.latitude - point1.latitude) * Math.PI) / 180;
const deltaLon = ((point2.longitude - point1.longitude) * Math.PI) / 180;
const a =
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}