• Make a request to the AniList API.

    Supports dynamic mutations where variable declarations may change based on the variables object passed. Handles both browser and Electron environments.

    Type Parameters

    • T

    Parameters

    • query: string

      The GraphQL query or mutation string.

    • Optionalvariables: Record<string, unknown>

      Optional variables for the query.

    • Optionaltoken: string

      Optional authentication token.

    • OptionalabortSignal: AbortSignal

      Optional abort signal to cancel the request.

    • OptionalbypassCache: boolean

      Optional flag to bypass cache.

    Returns Promise<AniListResponse<T>>

    A promise resolving to an AniListResponse object.

    export async function request<T>(
    query: string,
    variables?: Record<string, unknown>,
    token?: string,
    abortSignal?: AbortSignal,
    bypassCache?: boolean,
    ): Promise<AniListResponse<T>> {
    // Generate a unique request ID for tracking this request in logs
    const requestId = Math.random().toString(36).substring(2, 8);

    // Check if we're running in a browser or Electron environment
    const isElectron = typeof window !== "undefined" && window.electronAPI;

    // Create request options
    const options: RequestInit = {
    method: "POST",
    headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
    },
    body: JSON.stringify({
    query,
    variables,
    }),
    signal: abortSignal,
    };

    // Add authorization header if token is provided
    if (token) {
    options.headers = {
    ...options.headers,
    Authorization: `Bearer ${token}`,
    };
    }

    // Always log requests in development and production for debugging
    const queryFirstLine = query.trim().split("\n")[0].substring(0, 50);
    console.log(`📡 [${requestId}] GraphQL Request: ${queryFirstLine}...`);
    console.log(
    `📦 [${requestId}] Variables:`,
    JSON.stringify(variables, null, 2),
    );
    console.log(
    `📜 [${requestId}] Full Query:`,
    query.replace(/\s+/g, " ").trim(),
    );

    // For Electron, use IPC to make the request through the main process
    if (isElectron) {
    try {
    // Use the correct call format for the main process
    const response = await window.electronAPI.anilist.request(
    query,
    { ...variables, bypassCache }, // Pass bypassCache flag to main process
    token,
    // We can't pass AbortSignal through IPC, but we'll check it after
    );

    // Check for abort before returning the response
    if (abortSignal?.aborted) {
    throw new DOMException("The operation was aborted", "AbortError");
    }

    // Log response for debugging
    console.log(
    `✅ [${requestId}] Response received:`,
    JSON.stringify(response, null, 2),
    );

    return response as AniListResponse<T>;
    } catch (error) {
    console.error(
    `❌ [${requestId}] Error during AniList API request:`,
    error,
    );
    throw error;
    }
    }
    // For browser environment, use fetch directly
    else {
    try {
    const response = await fetch("https://graphql.anilist.co", options);

    if (!response.ok) {
    const errorText = await response.text();
    let errorData;
    try {
    errorData = JSON.parse(errorText);
    } catch {
    errorData = { raw: errorText };
    }

    console.error(
    `❌ [${requestId}] HTTP Error ${response.status}:`,
    errorData,
    );

    // Check for rate limiting
    if (response.status === 429) {
    // Extract the retry-after header
    const retryAfter = response.headers.get("Retry-After");
    const retrySeconds = retryAfter ? parseInt(retryAfter, 10) : 60;

    // Notify the application about rate limiting through a custom event
    try {
    // Dispatch global event for rate limiting that can be caught by our context
    window.dispatchEvent(
    new CustomEvent("anilist:rate-limited", {
    detail: {
    retryAfter: retrySeconds,
    message: `Rate limited by AniList API. Please retry after ${retrySeconds} seconds.`,
    },
    }),
    );
    } catch (e) {
    console.error("Failed to dispatch rate limit event:", e);
    }

    const error = {
    status: response.status,
    statusText: response.statusText,
    message: `Rate limit exceeded. Please retry after ${retrySeconds} seconds.`,
    retryAfter: retrySeconds,
    isRateLimited: true,
    ...errorData,
    };

    console.warn(
    `⏳ [${requestId}] Rate limited, retry after ${retrySeconds}s`,
    );
    throw error;
    }

    throw {
    status: response.status,
    statusText: response.statusText,
    ...errorData,
    };
    }

    const jsonResponse = await response.json();

    // Log response for debugging
    console.log(
    `✅ [${requestId}] Response received:`,
    JSON.stringify(jsonResponse, null, 2),
    );

    // Check for GraphQL errors
    if (jsonResponse.errors) {
    console.error(`⚠️ [${requestId}] GraphQL Errors:`, jsonResponse.errors);
    }

    return jsonResponse as AniListResponse<T>;
    } catch (error) {
    console.error(
    `❌ [${requestId}] Error during AniList API request:`,
    error,
    );
    throw error;
    }
    }
    }