Files
Cli-Proxy-API-Management-Ce…/src/stores/useUsageStatsStore.ts
T

144 lines
4.0 KiB
TypeScript

import { create } from 'zustand';
import { usageApi } from '@/services/api';
import { useAuthStore } from '@/stores/useAuthStore';
import { collectUsageDetails, computeKeyStats, type KeyStats, type UsageDetail } from '@/utils/usage';
import i18n from '@/i18n';
export const USAGE_STATS_STALE_TIME_MS = 240_000;
export type LoadUsageStatsOptions = {
force?: boolean;
staleTimeMs?: number;
};
type UsageStatsSnapshot = Record<string, unknown>;
type UsageStatsState = {
usage: UsageStatsSnapshot | null;
keyStats: KeyStats;
usageDetails: UsageDetail[];
loading: boolean;
error: string | null;
lastRefreshedAt: number | null;
scopeKey: string;
loadUsageStats: (options?: LoadUsageStatsOptions) => Promise<void>;
clearUsageStats: () => void;
};
const createEmptyKeyStats = (): KeyStats => ({ bySource: {}, byAuthIndex: {} });
let usageRequestToken = 0;
let inFlightUsageRequest: { id: number; scopeKey: string; promise: Promise<void> } | null = null;
const getErrorMessage = (error: unknown) =>
error instanceof Error
? error.message
: typeof error === 'string'
? error
: i18n.t('usage_stats.loading_error');
export const useUsageStatsStore = create<UsageStatsState>((set, get) => ({
usage: null,
keyStats: createEmptyKeyStats(),
usageDetails: [],
loading: false,
error: null,
lastRefreshedAt: null,
scopeKey: '',
loadUsageStats: async (options = {}) => {
const force = options.force === true;
const staleTimeMs = options.staleTimeMs ?? USAGE_STATS_STALE_TIME_MS;
const { apiBase = '', managementKey = '' } = useAuthStore.getState();
const scopeKey = `${apiBase}::${managementKey}`;
const state = get();
const scopeChanged = state.scopeKey !== scopeKey;
// 先复用同源 in-flight 请求,避免多个页面同时发起重复 /usage。
if (inFlightUsageRequest && inFlightUsageRequest.scopeKey === scopeKey) {
await inFlightUsageRequest.promise;
return;
}
// 连接目标变化时,旧请求结果必须失效。
if (inFlightUsageRequest && inFlightUsageRequest.scopeKey !== scopeKey) {
usageRequestToken += 1;
inFlightUsageRequest = null;
}
const fresh =
!scopeChanged &&
state.lastRefreshedAt !== null &&
Date.now() - state.lastRefreshedAt < staleTimeMs;
if (!force && fresh) {
return;
}
if (scopeChanged) {
set({
usage: null,
keyStats: createEmptyKeyStats(),
usageDetails: [],
error: null,
lastRefreshedAt: null,
scopeKey
});
}
const requestId = (usageRequestToken += 1);
set({ loading: true, error: null, scopeKey });
const requestPromise = (async () => {
try {
const usageResponse = await usageApi.getUsage();
const rawUsage = usageResponse?.usage ?? usageResponse;
const usage =
rawUsage && typeof rawUsage === 'object' ? (rawUsage as UsageStatsSnapshot) : null;
if (requestId !== usageRequestToken) return;
set({
usage,
keyStats: computeKeyStats(usage),
usageDetails: collectUsageDetails(usage),
loading: false,
error: null,
lastRefreshedAt: Date.now(),
scopeKey
});
} catch (error: unknown) {
if (requestId !== usageRequestToken) return;
const message = getErrorMessage(error);
set({
loading: false,
error: message,
scopeKey
});
throw new Error(message);
} finally {
if (inFlightUsageRequest?.id === requestId) {
inFlightUsageRequest = null;
}
}
})();
inFlightUsageRequest = { id: requestId, scopeKey, promise: requestPromise };
await requestPromise;
},
clearUsageStats: () => {
usageRequestToken += 1;
inFlightUsageRequest = null;
set({
usage: null,
keyStats: createEmptyKeyStats(),
usageDetails: [],
loading: false,
error: null,
lastRefreshedAt: null,
scopeKey: ''
});
}
}));