← back to Open Seo
src/client/features/backlinks/useAhrefsDomainRatings.ts
67 lines
import { useCallback, useEffect, useRef, useState } from "react";
import { chunk, unique } from "remeda";
import { toast } from "sonner";
import { getStandardErrorMessage } from "@/client/lib/error-messages";
import { getAhrefsDomainRatings } from "@/serverFunctions/ahrefs";
/** Map of domain (as held in table rows) → Ahrefs DR, or null when unknown. */
export type DomainRatings = Record<string, number | null>;
// The server function caps each call at 100 domains (Workers subrequest limit),
// so the client chunks larger sets and calls sequentially.
const DOMAINS_PER_REQUEST = 100;
export function useAhrefsDomainRatings(projectId: string) {
const [ratings, setRatings] = useState<DomainRatings | null>(null);
const ratingsRef = useRef<DomainRatings | null>(null);
const pendingDomainsRef = useRef(new Set<string>());
const [activeLoadCount, setActiveLoadCount] = useState(0);
useEffect(() => {
ratingsRef.current = ratings;
}, [ratings]);
const loadRatings = useCallback(
async (domains: string[]) => {
const currentRatings = ratingsRef.current;
const pendingDomains = pendingDomainsRef.current;
const targets = unique(domains.filter(Boolean)).filter(
(domain) =>
!Object.hasOwn(currentRatings ?? {}, domain) &&
!pendingDomains.has(domain),
);
if (targets.length === 0) return;
for (const domain of targets) pendingDomains.add(domain);
setActiveLoadCount((count) => count + 1);
const fetched: DomainRatings = {};
try {
for (const batch of chunk(targets, DOMAINS_PER_REQUEST)) {
Object.assign(
fetched,
await getAhrefsDomainRatings({
data: { projectId, domains: batch },
}),
);
}
} catch (error) {
// Opt-in convenience feature — surface partial results, don't crash.
toast.error(
getStandardErrorMessage(error, "Could not load Ahrefs DR."),
);
} finally {
if (Object.keys(fetched).length > 0) {
const nextRatings = { ...ratingsRef.current, ...fetched };
ratingsRef.current = nextRatings;
setRatings(nextRatings);
}
for (const domain of targets) pendingDomains.delete(domain);
setActiveLoadCount((count) => Math.max(0, count - 1));
}
},
[projectId],
);
return { ratings, isLoading: activeLoadCount > 0, loadRatings };
}