mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-24 02:39:17 -05:00
135 lines
5.1 KiB
TypeScript
135 lines
5.1 KiB
TypeScript
/**
|
|
* Simple API client for TanStack Query integration
|
|
*
|
|
* IMPORTANT: The Fetch API automatically handles ETags and HTTP caching for bandwidth optimization.
|
|
* We do NOT explicitly handle 304 responses because:
|
|
* 1. The browser's native HTTP cache handles If-None-Match headers automatically
|
|
* 2. When server returns 304, fetch returns the cached stored response (typically as 200) and updates cache headers
|
|
* 3. TanStack Query manages data freshness through staleTime configuration
|
|
*
|
|
* This simplification eliminates complex ETag management while maintaining bandwidth efficiency.
|
|
* For cache control, configure TanStack Query's staleTime/gcTime instead of manual HTTP caching.
|
|
*/
|
|
|
|
import { API_BASE_URL } from "../../../config/api";
|
|
import { APIServiceError } from "../types/errors";
|
|
|
|
/**
|
|
* Build full URL with test environment handling
|
|
* Ensures consistent URL construction for cache keys
|
|
*/
|
|
function buildFullUrl(cleanEndpoint: string): string {
|
|
let fullUrl = `${API_BASE_URL}${cleanEndpoint}`;
|
|
|
|
// Only convert to absolute URL in test environment
|
|
const isTestEnv = typeof process !== "undefined" && process.env?.NODE_ENV === "test";
|
|
|
|
if (isTestEnv && !fullUrl.startsWith("http")) {
|
|
const testHost = "localhost";
|
|
const testPort = process.env?.ARCHON_SERVER_PORT || "8181";
|
|
fullUrl = `http://${testHost}:${testPort}${fullUrl}`;
|
|
}
|
|
|
|
return fullUrl;
|
|
}
|
|
|
|
/**
|
|
* Simple API call function for JSON APIs
|
|
* Browser automatically handles ETags/304s through its HTTP cache
|
|
*
|
|
* NOTE: This wrapper is designed for JSON-only API calls.
|
|
* For file uploads or FormData requests, use fetch() directly.
|
|
*/
|
|
export async function callAPIWithETag<T = unknown>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
try {
|
|
// Handle absolute URLs (direct service connections)
|
|
const isAbsoluteUrl = endpoint.startsWith("http://") || endpoint.startsWith("https://");
|
|
|
|
let fullUrl: string;
|
|
if (isAbsoluteUrl) {
|
|
// Use absolute URL as-is (for direct service connections)
|
|
fullUrl = endpoint;
|
|
} else {
|
|
// Clean endpoint and build relative URL
|
|
const cleanEndpoint = endpoint.startsWith("/api") ? endpoint.substring(4) : endpoint;
|
|
fullUrl = buildFullUrl(cleanEndpoint);
|
|
}
|
|
|
|
// Build headers - only set Content-Type for requests with a body
|
|
// NOTE: We do NOT add If-None-Match headers; the browser handles ETag revalidation automatically
|
|
//
|
|
// Currently assumes headers are passed as plain objects (Record<string, string>)
|
|
// which works for all our current usage. The API doesn't require Accept headers
|
|
// since it always returns JSON, and we only set Content-Type when sending data.
|
|
const headers: Record<string, string> = {
|
|
...((options.headers as Record<string, string>) || {}),
|
|
};
|
|
|
|
// Only set Content-Type for requests that have a body (POST, PUT, PATCH, etc.)
|
|
// GET and DELETE requests should not have Content-Type header
|
|
const _method = options.method?.toUpperCase() || "GET";
|
|
const hasBody = options.body !== undefined && options.body !== null;
|
|
if (hasBody && !headers["Content-Type"]) {
|
|
headers["Content-Type"] = "application/json";
|
|
}
|
|
|
|
// Make the request with timeout
|
|
// NOTE: Increased to 20s due to database performance issues with large DELETE operations
|
|
// Root cause: Sequential scan on crawled_pages table when deleting sources with 7K+ rows
|
|
// takes 13+ seconds. This is a temporary fix until we implement batch deletion.
|
|
// See: DELETE FROM archon_crawled_pages WHERE source_id = '9529d5dabe8a726a' (7,073 rows)
|
|
const response = await fetch(fullUrl, {
|
|
...options,
|
|
headers,
|
|
signal: options.signal ?? AbortSignal.timeout(20000), // 20 second timeout (was 10s)
|
|
});
|
|
|
|
// Handle errors
|
|
if (!response.ok) {
|
|
let errorMessage = `HTTP error! status: ${response.status}`;
|
|
try {
|
|
const errorBody = await response.text();
|
|
if (errorBody) {
|
|
const errorJson = JSON.parse(errorBody);
|
|
// Handle nested error structure from backend {"detail": {"error": "message"}}
|
|
if (typeof errorJson.detail === "object" && errorJson.detail !== null && "error" in errorJson.detail) {
|
|
errorMessage = errorJson.detail.error;
|
|
} else if (errorJson.detail) {
|
|
errorMessage = errorJson.detail;
|
|
} else if (errorJson.error) {
|
|
errorMessage = errorJson.error;
|
|
}
|
|
}
|
|
} catch (_e) {
|
|
// Ignore parse errors
|
|
}
|
|
throw new APIServiceError(errorMessage, "HTTP_ERROR", response.status);
|
|
}
|
|
|
|
// Handle 204 No Content (DELETE operations)
|
|
if (response.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
// Parse response data
|
|
const result = await response.json();
|
|
|
|
// Check for API errors
|
|
if (result.error) {
|
|
throw new APIServiceError(result.error, "API_ERROR", response.status);
|
|
}
|
|
|
|
return result as T;
|
|
} catch (error) {
|
|
if (error instanceof APIServiceError) {
|
|
throw error;
|
|
}
|
|
|
|
throw new APIServiceError(
|
|
`Failed to call API ${endpoint}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
"NETWORK_ERROR",
|
|
500,
|
|
);
|
|
}
|
|
}
|