fix: flaky WebSocket test, usage request queue, and test improvements
- Fix flaky WebSocket passthrough test: allow StatusNormalClosure after client close instead of requiring NoError (race condition fix) - Fix ratelimit 401 test: use PlatformOpenAI instead of PlatformGemini for OAuth token cache invalidation scenario (more accurate) - Add usageLoadQueue: Anthropic OAuth/setup-token accounts sharing the same proxy exit are serialized with 1-2s jitter to prevent upstream 429 - AccountUsageCell: add module-level usage cache (5min TTL), unmounted safety guard, and integrate enqueueUsageRequest for throttled fetching
This commit is contained in:
@@ -439,15 +439,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, AccountUsageInfo, GeminiCredentials, WindowStats } from '@/types'
|
||||
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
|
||||
import { enqueueUsageRequest } from '@/utils/usageLoadQueue'
|
||||
import { formatCompactNumber } from '@/utils/format'
|
||||
import UsageProgressBar from './UsageProgressBar.vue'
|
||||
import AccountQuotaInfo from './AccountQuotaInfo.vue'
|
||||
|
||||
// Module-level cache shared across all AccountUsageCell instances
|
||||
const _usageCache = new Map<number, { data: AccountUsageInfo; ts: number }>()
|
||||
const USAGE_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
account: Account
|
||||
@@ -465,6 +470,9 @@ const props = withDefaults(
|
||||
const { t } = useI18n()
|
||||
const desktopViewportQuery = '(min-width: 768px)'
|
||||
|
||||
const unmounted = ref(false)
|
||||
onBeforeUnmount(() => { unmounted.value = true })
|
||||
|
||||
const loading = ref(false)
|
||||
const activeQueryLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
@@ -941,19 +949,36 @@ const isAnthropicOAuthOrSetupToken = computed(() => {
|
||||
return props.account.platform === 'anthropic' && (props.account.type === 'oauth' || props.account.type === 'setup-token')
|
||||
})
|
||||
|
||||
const loadUsage = async (source?: 'passive' | 'active') => {
|
||||
const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?: boolean }) => {
|
||||
if (!shouldFetchUsage.value) return
|
||||
|
||||
// Check cache
|
||||
if (!options?.bypassCache) {
|
||||
const cached = _usageCache.get(props.account.id)
|
||||
if (cached && Date.now() - cached.ts < USAGE_CACHE_TTL) {
|
||||
usageInfo.value = cached.data
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id, source)
|
||||
const fetchFn = () => adminAPI.accounts.getUsage(props.account.id, options?.source)
|
||||
const result = await enqueueUsageRequest(props.account, fetchFn)
|
||||
if (!unmounted.value) {
|
||||
usageInfo.value = result
|
||||
_usageCache.set(props.account.id, { data: result, ts: Date.now() })
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = t('common.error')
|
||||
console.error('Failed to load usage:', e)
|
||||
if (!unmounted.value) {
|
||||
error.value = t('common.error')
|
||||
console.error('Failed to load usage:', e)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (!unmounted.value) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,7 +987,7 @@ const flushPendingAutoLoad = () => {
|
||||
const source = pendingAutoLoadSource.value
|
||||
pendingAutoLoad.value = false
|
||||
pendingAutoLoadSource.value = undefined
|
||||
loadUsage(source).catch((e) => {
|
||||
loadUsage({ source }).catch((e) => {
|
||||
console.error('Failed to load deferred usage:', e)
|
||||
})
|
||||
}
|
||||
@@ -974,7 +999,7 @@ const requestAutoLoad = (source?: 'passive' | 'active') => {
|
||||
pendingAutoLoadSource.value = source
|
||||
return
|
||||
}
|
||||
loadUsage(source).catch((e) => {
|
||||
loadUsage({ source }).catch((e) => {
|
||||
console.error('Failed to auto load usage:', e)
|
||||
})
|
||||
}
|
||||
@@ -1138,7 +1163,10 @@ watch(
|
||||
if (!shouldFetchUsage.value) return
|
||||
|
||||
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
|
||||
requestAutoLoad(source)
|
||||
_usageCache.delete(props.account.id)
|
||||
loadUsage({ source, bypassCache: true }).catch((e) => {
|
||||
console.error('Failed to refresh usage after manual refresh:', e)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user