import { useCallback, useEffect, useRef, useState } from "react"; import { fetchClaudeOrgs, fetchClaudeUsage, parseClaudeUsageResponse, } from "@/lib/api/claudeApi"; import { isClaudeSessionKeyValid, validateClaudeSessionKey, } from "@/lib/claudeCredentials"; import { RETRY_DELAY_MS } from "@/lib/constants"; import { clearClaudeLastActiveOrg, clearClaudeSessionKey, clearClaudeUsageCache, loadClaudeLastActiveOrg, loadClaudeSessionKey, loadClaudeUsageCache, saveClaudeLastActiveOrg, saveClaudeSessionKey, saveClaudeUsageCache, } from "@/lib/storage"; import type { ClaudeAuth, ClaudeUsageResponse } from "@/types/claude"; type Status = "idle" | "loading" | "success" | "error"; function shouldRetry(error: unknown): boolean { const message = error instanceof Error ? error.message : ""; return ![ "TOKEN_EXPIRED", "NO_ORGS_FOUND", "INVALID_CLAUDE_RESPONSE", "INVALID_CLAUDE_ORGS_RESPONSE", ].includes(message); } function wait(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } async function requestClaudeUsage(auth: ClaudeAuth): Promise { let orgUuid = auth.lastActiveOrg?.trim(); if (!orgUuid) { const orgs = await fetchClaudeOrgs(auth); if (!orgs.length) throw new Error("NO_ORGS_FOUND"); orgUuid = orgs[0].uuid; await saveClaudeLastActiveOrg(orgUuid); } return fetchClaudeUsage(auth, orgUuid); } export function useClaudeUsage() { const [auth, setAuth] = useState(null); const [pendingKey, setPendingKey] = useState(""); const [pendingOrg, setPendingOrg] = useState(""); const [usage, setUsage] = useState(null); const [lastFetchedAt, setLastFetchedAt] = useState(null); const [status, setStatus] = useState("idle"); const [error, setError] = useState(null); const mounted = useRef(true); const inFlight = useRef<{ key: string; promise: Promise; } | null>(null); const requestVersion = useRef(0); useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); const fetchUsage = useCallback((credentials: ClaudeAuth): Promise => { const sanitized = { sessionKey: credentials.sessionKey.trim(), lastActiveOrg: credentials.lastActiveOrg?.trim() || undefined, }; const requestKey = `${sanitized.lastActiveOrg ?? ""}:${sanitized.sessionKey}`; if (inFlight.current?.key === requestKey) return inFlight.current.promise; const version = ++requestVersion.current; const promise = (async () => { if (mounted.current) { setStatus("loading"); setError(null); } try { let data: ClaudeUsageResponse; try { data = await requestClaudeUsage(sanitized); } catch (firstError) { if (!shouldRetry(firstError)) throw firstError; await wait(RETRY_DELAY_MS); if (version !== requestVersion.current) return; data = await requestClaudeUsage(sanitized); } if (version !== requestVersion.current) return; const fetchedAt = Date.now(); await saveClaudeUsageCache({ data, lastFetchedAt: fetchedAt }); if (mounted.current && version === requestVersion.current) { setUsage(data); setLastFetchedAt(fetchedAt); setStatus("success"); } } catch (caught: unknown) { if (version !== requestVersion.current) return; const message = caught instanceof Error ? caught.message : "UNKNOWN_ERROR"; if (message === "TOKEN_EXPIRED") { await clearClaudeSessionKey(); if (mounted.current) { setAuth(null); setUsage(null); setLastFetchedAt(null); } } if (mounted.current) { setError(message); setStatus("error"); } } })().finally(() => { if (inFlight.current?.promise === promise) inFlight.current = null; }); inFlight.current = { key: requestKey, promise }; return promise; }, []); useEffect(() => { let active = true; void Promise.all([ loadClaudeSessionKey(), loadClaudeLastActiveOrg(), loadClaudeUsageCache(), ]).then(async ([key, org, cache]) => { if (!active) return; const storedAuth = key ? { sessionKey: key.trim(), lastActiveOrg: org?.trim() || undefined } : null; if (storedAuth && cache) { try { if (!Number.isFinite(cache.lastFetchedAt)) { throw new Error("INVALID_CACHE_TIMESTAMP"); } const cachedUsage = parseClaudeUsageResponse(cache.data); setUsage(cachedUsage); setLastFetchedAt(cache.lastFetchedAt); setStatus("success"); } catch { await clearClaudeUsageCache(); } } setAuth(storedAuth); if (storedAuth) await fetchUsage(storedAuth); }); return () => { active = false; }; }, [fetchUsage]); const saveKey = useCallback(async () => { const trimmedKey = pendingKey.trim(); const validationError = validateClaudeSessionKey(trimmedKey); if (validationError) { setError(validationError); return; } const trimmedOrg = pendingOrg.trim() || undefined; await saveClaudeSessionKey(trimmedKey); if (trimmedOrg) await saveClaudeLastActiveOrg(trimmedOrg); else await clearClaudeLastActiveOrg(); const nextAuth = { sessionKey: trimmedKey, lastActiveOrg: trimmedOrg }; setAuth(nextAuth); setPendingKey(""); setPendingOrg(""); setError(null); await fetchUsage(nextAuth); }, [fetchUsage, pendingKey, pendingOrg]); const refresh = useCallback(async () => { if (auth) await fetchUsage(auth); }, [auth, fetchUsage]); const reloadCredentials = useCallback(async () => { const [key, org] = await Promise.all([ loadClaudeSessionKey(), loadClaudeLastActiveOrg(), ]); if (!key) { requestVersion.current += 1; setAuth(null); setUsage(null); setLastFetchedAt(null); setStatus("idle"); return; } const stored = { sessionKey: key.trim(), lastActiveOrg: org?.trim() || undefined, }; setAuth(stored); await fetchUsage(stored); }, [fetchUsage]); const clearKey = useCallback(async () => { requestVersion.current += 1; await clearClaudeSessionKey(); setAuth(null); setUsage(null); setLastFetchedAt(null); setStatus("idle"); setError(null); }, []); return { auth, pendingKey, setPendingKey, pendingOrg, setPendingOrg, usage, lastFetchedAt, status, error, keyValidationError: pendingKey ? validateClaudeSessionKey(pendingKey) : null, canSaveKey: isClaudeSessionKeyValid(pendingKey), saveKey, refresh, reloadCredentials, clearKey, }; }