Files
Stirling-PDF/frontend/src/saas/hooks/useEndpointConfig.ts
T
James Brunton 8ab060a4be Prettier 2: Electric Boogaloo (#6113)
# Description of Changes
When I added Prettier formatting in #6052, my aim was to use just the
default settings in Prettier. Turns out, Prettier looks _really hard_
for any config files if it's not explicitly given one, which means that
if a developer has some sort of Prettier config file lying around on
their system, Prettier might find it and use it. Also, Prettier changes
its defaults based on stuff in `.editorconfig` without any good way of
disabling that behaviour explicitly in its config file.

To solve both of these issues, I've introduced a `.prettierrc` file
which sets Prettier's defaults explicitly, and then reformatted all our
code _again_ in Prettier's actual default settings. This should achieve
the aim of #6052 and remove the possibility for it breaking on different
dev computers.
2026-04-17 09:50:16 +00:00

256 lines
8.0 KiB
TypeScript

import { useState, useEffect } from "react";
import { isAxiosError } from "axios";
import apiClient from "@app/services/apiClient";
import type { EndpointAvailabilityDetails } from "@app/types/endpointAvailability";
// Track globally fetched endpoint sets to prevent duplicate fetches across components
const globalFetchedSets = new Set<string>();
const globalEndpointCache: Record<string, EndpointAvailabilityDetails> = {};
/**
* Hook to check if a specific endpoint is enabled
* This wraps the context for single endpoint checks
*/
export function useEndpointEnabled(endpoint: string): {
enabled: boolean | null;
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
} {
const [enabled, setEnabled] = useState<boolean | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchEndpointStatus = async () => {
if (!endpoint) {
setEnabled(null);
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
const response = await apiClient.get<boolean>(
`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`,
);
const isEnabled = response.data;
setEnabled(isEnabled);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Unknown error occurred";
setError(errorMessage);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchEndpointStatus();
}, [endpoint]);
return {
enabled,
loading,
error,
refetch: fetchEndpointStatus,
};
}
/**
* Hook to check multiple endpoints at once using batch API
* Returns a map of endpoint -> enabled status
*/
export function useMultipleEndpointsEnabled(endpoints: string[]): {
endpointStatus: Record<string, boolean>;
endpointDetails: Record<string, EndpointAvailabilityDetails>;
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
} {
const [endpointStatus, setEndpointStatus] = useState<Record<string, boolean>>(
{},
);
const [endpointDetails, setEndpointDetails] = useState<
Record<string, EndpointAvailabilityDetails>
>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchAllEndpointStatuses = async (force = false) => {
const endpointsKey = [...endpoints].sort().join(",");
// Skip if we already fetched these exact endpoints globally
if (!force && globalFetchedSets.has(endpointsKey)) {
console.debug(
"[useEndpointConfig] Already fetched these endpoints globally, using cache",
);
const cached = endpoints.reduce(
(acc, endpoint) => {
const cachedDetails = globalEndpointCache[endpoint];
if (cachedDetails) {
acc.status[endpoint] = cachedDetails.enabled;
acc.details[endpoint] = cachedDetails;
}
return acc;
},
{
status: {} as Record<string, boolean>,
details: {} as Record<string, EndpointAvailabilityDetails>,
},
);
setEndpointStatus(cached.status);
setEndpointDetails((prev) => ({ ...prev, ...cached.details }));
setLoading(false);
return;
}
if (!endpoints || endpoints.length === 0) {
setEndpointStatus({});
setEndpointDetails({});
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
// Check which endpoints we haven't fetched yet
const newEndpoints = endpoints.filter(
(ep) => !(ep in globalEndpointCache),
);
if (newEndpoints.length === 0) {
console.debug(
"[useEndpointConfig] All endpoints already in global cache",
);
const cached = endpoints.reduce(
(acc, endpoint) => {
const cachedDetails = globalEndpointCache[endpoint];
if (cachedDetails) {
acc.status[endpoint] = cachedDetails.enabled;
acc.details[endpoint] = cachedDetails;
}
return acc;
},
{
status: {} as Record<string, boolean>,
details: {} as Record<string, EndpointAvailabilityDetails>,
},
);
setEndpointStatus(cached.status);
setEndpointDetails((prev) => ({ ...prev, ...cached.details }));
globalFetchedSets.add(endpointsKey);
setLoading(false);
return;
}
// Use batch API for efficiency - only fetch new endpoints
const endpointsParam = newEndpoints.join(",");
const response = await apiClient.get<
Record<string, EndpointAvailabilityDetails>
>(
`/api/v1/config/endpoints-availability?endpoints=${encodeURIComponent(endpointsParam)}`,
);
const statusMap = response.data;
// Update global cache with new results
Object.entries(statusMap).forEach(([endpoint, details]) => {
globalEndpointCache[endpoint] = {
enabled: details?.enabled ?? true,
reason: details?.reason ?? null,
};
});
// Get all requested endpoints from cache (including previously cached ones)
const fullStatus = endpoints.reduce(
(acc, endpoint) => {
const cachedDetails = globalEndpointCache[endpoint];
if (cachedDetails) {
acc.status[endpoint] = cachedDetails.enabled;
acc.details[endpoint] = cachedDetails;
} else {
acc.status[endpoint] = true;
}
return acc;
},
{
status: {} as Record<string, boolean>,
details: {} as Record<string, EndpointAvailabilityDetails>,
},
);
setEndpointStatus(fullStatus.status);
setEndpointDetails((prev) => ({ ...prev, ...fullStatus.details }));
globalFetchedSets.add(endpointsKey);
} catch (err: unknown) {
// On 401 (auth error), use optimistic fallback instead of disabling
if (isAxiosError(err) && err.response?.status === 401) {
console.warn(
"[useEndpointConfig] 401 error - using optimistic fallback",
);
const optimisticStatus = endpoints.reduce(
(acc, endpoint) => {
const optimisticDetails: EndpointAvailabilityDetails = {
enabled: true,
reason: null,
};
acc.status[endpoint] = true;
acc.details[endpoint] = optimisticDetails;
globalEndpointCache[endpoint] = optimisticDetails;
return acc;
},
{
status: {} as Record<string, boolean>,
details: {} as Record<string, EndpointAvailabilityDetails>,
},
);
setEndpointStatus(optimisticStatus.status);
setEndpointDetails((prev) => ({
...prev,
...optimisticStatus.details,
}));
setLoading(false);
return;
}
const errorMessage =
err instanceof Error ? err.message : "Unknown error occurred";
setError(errorMessage);
console.error(
"[EndpointConfig] Failed to check multiple endpoints:",
err,
);
// Fallback: assume all endpoints are enabled on error (optimistic)
const optimisticStatus = endpoints.reduce(
(acc, endpoint) => {
const optimisticDetails: EndpointAvailabilityDetails = {
enabled: true,
reason: null,
};
acc.status[endpoint] = true;
acc.details[endpoint] = optimisticDetails;
return acc;
},
{
status: {} as Record<string, boolean>,
details: {} as Record<string, EndpointAvailabilityDetails>,
},
);
setEndpointStatus(optimisticStatus.status);
setEndpointDetails((prev) => ({ ...prev, ...optimisticStatus.details }));
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchAllEndpointStatuses();
}, [endpoints.join(",")]); // Re-run when endpoints array changes
return {
endpointStatus,
endpointDetails,
loading,
error,
refetch: () => fetchAllEndpointStatuses(true),
};
}