mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
144 lines
4.0 KiB
TypeScript
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: ''
|
|
});
|
|
}
|
|
}));
|