by codex: feat. General Improvements
This commit is contained in:
+182
-61
@@ -1,47 +1,52 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { fetchClaudeOrgs, fetchClaudeUsage } from "@/lib/api/claudeApi";
|
||||
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 {
|
||||
loadClaudeSessionKey,
|
||||
saveClaudeSessionKey,
|
||||
clearClaudeSessionKey,
|
||||
loadClaudeLastActiveOrg,
|
||||
saveClaudeLastActiveOrg,
|
||||
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";
|
||||
|
||||
async function doFetch(
|
||||
auth: ClaudeAuth,
|
||||
set: {
|
||||
auth: (v: ClaudeAuth | null) => void;
|
||||
usage: (v: ClaudeUsageResponse | null) => void;
|
||||
status: (v: Status) => void;
|
||||
error: (v: string | null) => void;
|
||||
}
|
||||
) {
|
||||
set.status("loading");
|
||||
set.error(null);
|
||||
try {
|
||||
let orgUuid = auth.lastActiveOrg;
|
||||
if (!orgUuid) {
|
||||
const orgs = await fetchClaudeOrgs(auth);
|
||||
if (!orgs.length) throw new Error("NO_ORGS_FOUND");
|
||||
orgUuid = orgs[0].uuid;
|
||||
}
|
||||
const data = await fetchClaudeUsage(auth, orgUuid);
|
||||
set.usage(data);
|
||||
set.status("success");
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR";
|
||||
set.error(msg);
|
||||
set.status("error");
|
||||
if (msg === "TOKEN_EXPIRED") {
|
||||
await clearClaudeSessionKey();
|
||||
set.auth(null);
|
||||
}
|
||||
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<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function requestClaudeUsage(auth: ClaudeAuth): Promise<ClaudeUsageResponse> {
|
||||
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() {
|
||||
@@ -49,60 +54,171 @@ export function useClaudeUsage() {
|
||||
const [pendingKey, setPendingKey] = useState("");
|
||||
const [pendingOrg, setPendingOrg] = useState("");
|
||||
const [usage, setUsage] = useState<ClaudeUsageResponse | null>(null);
|
||||
const [lastFetchedAt, setLastFetchedAt] = useState<number | null>(null);
|
||||
const [status, setStatus] = useState<Status>("idle");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const setters = { auth: setAuth, usage: setUsage, status: setStatus, error: setError };
|
||||
const mounted = useRef(true);
|
||||
const inFlight = useRef<{
|
||||
key: string;
|
||||
promise: Promise<void>;
|
||||
} | null>(null);
|
||||
const requestVersion = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]).then(
|
||||
([key, org]) => {
|
||||
if (key) setAuth({ sessionKey: key, lastActiveOrg: org ?? undefined });
|
||||
mounted.current = true;
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchUsage = useCallback((credentials: ClaudeAuth): Promise<void> => {
|
||||
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(() => {
|
||||
if (auth) void doFetch(auth, setters);
|
||||
}, [auth]);
|
||||
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 trimmed = pendingKey.trim();
|
||||
if (!trimmed.startsWith("sk-ant-")) {
|
||||
setError("Session key must start with sk-ant-");
|
||||
const trimmedKey = pendingKey.trim();
|
||||
const validationError = validateClaudeSessionKey(trimmedKey);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
const orgTrimmed = pendingOrg.trim() || undefined;
|
||||
await saveClaudeSessionKey(trimmed);
|
||||
if (orgTrimmed) await saveClaudeLastActiveOrg(orgTrimmed);
|
||||
|
||||
const trimmedOrg = pendingOrg.trim() || undefined;
|
||||
await saveClaudeSessionKey(trimmedKey);
|
||||
if (trimmedOrg) await saveClaudeLastActiveOrg(trimmedOrg);
|
||||
else await clearClaudeLastActiveOrg();
|
||||
setAuth({ sessionKey: trimmed, lastActiveOrg: orgTrimmed });
|
||||
|
||||
const nextAuth = { sessionKey: trimmedKey, lastActiveOrg: trimmedOrg };
|
||||
setAuth(nextAuth);
|
||||
setPendingKey("");
|
||||
setPendingOrg("");
|
||||
setError(null);
|
||||
}, [pendingKey, pendingOrg]);
|
||||
await fetchUsage(nextAuth);
|
||||
}, [fetchUsage, pendingKey, pendingOrg]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!auth) return;
|
||||
await doFetch(auth, setters);
|
||||
}, [auth]);
|
||||
if (auth) await fetchUsage(auth);
|
||||
}, [auth, fetchUsage]);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
const [key, org] = await Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]);
|
||||
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;
|
||||
}
|
||||
// Creating a new object always triggers the [auth] useEffect even if values are identical
|
||||
setAuth({ sessionKey: key, lastActiveOrg: org ?? undefined });
|
||||
}, []);
|
||||
|
||||
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);
|
||||
}, []);
|
||||
@@ -114,11 +230,16 @@ export function useClaudeUsage() {
|
||||
pendingOrg,
|
||||
setPendingOrg,
|
||||
usage,
|
||||
lastFetchedAt,
|
||||
status,
|
||||
error,
|
||||
keyValidationError: pendingKey
|
||||
? validateClaudeSessionKey(pendingKey)
|
||||
: null,
|
||||
canSaveKey: isClaudeSessionKeyValid(pendingKey),
|
||||
saveKey,
|
||||
refresh,
|
||||
reload,
|
||||
reloadCredentials,
|
||||
clearKey,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user